Skip to content

feat: move auth to Coolify Postgres - #88

Merged
Skeyelab merged 5 commits into
masterfrom
feat/coolify-postgres-auth
Sep 23, 2026
Merged

Skeyelab merged 5 commits into
masterfrom
feat/coolify-postgres-auth

Conversation

@Skeyelab

Copy link
Copy Markdown
Member

Summary

  • Replaces Neon Auth with app-owned email/password auth backed by Postgres.
  • Adds auth_users and auth_sessions tables plus first-party auth API routes.
  • Adds startup SQL migration runner so Coolify private Postgres can be initialized on deploy.
  • Updates Docker/Coolify docs and removes the Neon Auth dependency/env requirements.
  • Creates .env.example for local/Coolify Postgres setup.

Coolify

  • Created private Coolify Postgres database clawproxy-postgres in the existing clawproxy production project.
  • Updated the clawproxy.io app DATABASE_URL env var to the new internal Coolify DB URL.
  • Database verified in Coolify as running:healthy and not public.

Test Plan

  • yarn lint
  • yarn test:run
  • DATABASE_URL='postgresql://user:pass@localhost:5432/clawproxy' ENCRYPTION_KEY='0000000000000000000000000000000000000000000000000000000000000000' yarn build

Notes

  • Existing stale Neon env vars remain in Coolify but are no longer read by the app.
  • The new database is private; migrations are applied automatically by the custom server before it starts listening.

@strix-security

strix-security Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Strix Security Review

All previously reported security findings have been resolved.

4 resolved findings
Review summary

Re-reviewed the full diff with focus on the app-owned email/password auth introduced by this PR. The previously open rate-limit finding is now resolved: recordAuthRateLimitAttempt performs a single atomic INSERT ... ON CONFLICT DO UPDATE whose count is incremented in the database (count + 1) with a matching conditional reset on window expiry, eliminating the lost-update bypass. The wider auth surface was also reviewed — session token generation and storage, password hashing (scrypt N=131072), sign-in/sign-up/sign-out/forgot-password routes, DB-backed rate limiting, schema and migration runner. No new security issues were identified; session cookies are properly scoped and hashed at rest, timing-safe scrypt verification is used with a dummy hash for missing users, and migration/SQL inputs are parameterized or safely escaped.

Updated for bdaf2c1.


Reviewed by Strix
Re-run review · Configure security review settings

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged 2 new security findings below. See the pinned summary comment for the full PR status.

Comment thread app/lib/auth/password.ts Outdated
import { randomBytes, scrypt as scryptCallback, timingSafeEqual, type ScryptOptions } from 'node:crypto';

const KEY_LENGTH = 64;
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 } as const;

@strix-security strix-security Bot Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak scrypt password-hashing parameters (N=16384) below OWASP recommendation

Resolved — no longer flagged as of bca647b.

Comment on lines +6 to +15
export async function POST(request: Request) {
const body = await readJson(request);
const result = await signInWithPassword(stringField(body, 'email'), stringField(body, 'password'));

if (!result.ok) {
return NextResponse.json({ ok: false, error: result.error }, { status: result.status ?? 401 });
}

return okWithSession({ user: result.user }, result.token, result.expiresAt);
}

@strix-security strix-security Bot Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign-in/sign-up throttling bypassable via spoofed X-Forwarded-For and per-process state

Resolved — no longer flagged as of 29948b0.

@Skeyelab

Copy link
Copy Markdown
Member Author

@strix-security please rerun the security review. Addressed both findings: raised scrypt to N=131072 with increased maxmem and added fixed-window throttling for sign-in/sign-up attempts.

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment thread app/lib/auth/server.ts Outdated
Comment on lines +105 to +107
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return { ok: false, error: 'Invalid email or password', status: 401 };
}

@strix-security strix-security Bot Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timing-based account enumeration on password sign-in endpoint

Resolved — no longer flagged as of 29948b0.

@Skeyelab

Copy link
Copy Markdown
Member Author

@strix-security please rerun again. Follow-up fixes: moved auth throttling into Postgres-backed counters keyed without X-Forwarded-For, added auth_rate_limits migration/table, and added dummy scrypt verification for missing users to reduce sign-in timing enumeration.

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment thread app/lib/auth/rate-limit.ts Outdated
@@ -0,0 +1,75 @@
import 'server-only';

import { eq } from 'drizzle-orm';

@strix-security strix-security Bot Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-atomic rate-limit counter allows concurrent requests to bypass the sign-in brute-force throttle

Resolved — no longer flagged as of bdaf2c1.

Comment on lines +48 to +71
export async function recordAuthRateLimitAttempt({
key,
kind,
windowMs,
}: Omit<AuthRateLimitOptions, 'maxAttempts'>) {
const [entry] = await db.select().from(authRateLimits).where(eq(authRateLimits.key, key)).limit(1);
const windowStart = new Date();

if (!entry || nowMs() - entry.windowStart.getTime() >= windowMs) {
await db
.insert(authRateLimits)
.values({ key, kind, count: 1, windowStart, updatedAt: windowStart })
.onConflictDoUpdate({
target: authRateLimits.key,
set: { kind, count: 1, windowStart, updatedAt: windowStart },
});
return;
}

await db
.update(authRateLimits)
.set({ count: entry.count + 1, updatedAt: windowStart })
.where(eq(authRateLimits.key, key));
}

@strix-security strix-security Bot Sep 23, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-atomic rate-limit counter allows concurrent requests to bypass the sign-in brute-force throttle

Resolved — no longer flagged as of bdaf2c1.

@Skeyelab

Copy link
Copy Markdown
Member Author

@strix-security please rerun once more. Fixed the remaining atomicity finding by changing rate-limit recording to a single Postgres upsert that increments count in the database.

@Skeyelab
Skeyelab merged commit 18c9b8a into master Sep 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant