Skip to content

security: harden server for public-internet multi-tenant deployment#10

Merged
mgorabbani merged 2 commits into
mainfrom
claude/security-audit-15WxT
Apr 20, 2026
Merged

security: harden server for public-internet multi-tenant deployment#10
mgorabbani merged 2 commits into
mainfrom
claude/security-audit-15WxT

Conversation

@mgorabbani

Copy link
Copy Markdown
Owner

Audits the self-hosted, multi-user, internet-facing deployment profile against
April 2026 OAuth BCP (RFC 9700), OAuth 2.1, and current secure-defaults.

Sandbox credentials (C1)

  • Every sandbox container now boots with a random AES-256-GCM-encrypted
    password persisted to connections.sandboxPassword before container
    creation, so a crash cannot leave a recoverable row that still grants
    access with the old 'askdb/askdb' literal.
  • Sandbox ports bind to 127.0.0.1 by default; SANDBOX_BIND_HOST env lets
    docker-in-docker deployments override when the server itself runs as a
    peer container and talks over host-gateway.
  • Legacy rows without a stored password keep working against the old
    container via a documented fallback until re-sync recreates them.

Adapter boundary (H1, M3)

  • assertValidDatabaseName('^[A-Za-z0-9_-]{1,63}$') gates all paths that
    interpolate a database name into shell args, URL pathnames, or quoted
    identifiers. Both mongodump/mongorestore and pg_dump/psql callers
    validate, and CREATE DATABASE drops its ad-hoc quote escaping now that
    the upstream validator rejects quotes entirely.
  • pg and mongo CLI stderr is scrubbed for URI userinfo and password= pairs
    before it lands in connections.sync_error.

OAuth (H2, M2, L2)

  • Redirect URI validation now matches RFC 9700 §4.1.3 and OAuth 2.1 §4.1.2:
    rejects fragments, userinfo, wildcards, oversize URIs, plain-http except
    loopback, and https pointing at RFC1918/link-local/CGNAT/metadata/ULA.
  • Refresh tokens rotate with a familyId and reuse-detection: presenting
    an already-revoked refresh token revokes the entire family AND all
    outstanding access tokens for that user+client pair, matching OAuth 2.1
    §4.2.2.
  • Default refresh-token TTL shortened from 30d to 7d.
  • New auth_audit_logs table records sign-in/sign-up outcomes, OAuth
    client registrations/updates, and refresh-token-reuse detections with
    IP/UA/timestamps for forensics.

Server transport (H4, M1)

  • helmet 8.1.0 applied with a tight CSP (self + inline styles only), 2y
    HSTS with includeSubDomains+preload, no-referrer, and COOP/CORP
    same-origin. First-party UI only; no third-party iframes.
  • /api/auth/sign-in and /api/auth/sign-up get a 5-req/15min limiter with
    skipSuccessfulRequests so legit credential-correct users aren't locked
    out mid-spray. draft-8 RateLimit-* headers only.

Schema migrations

  • Idempotent ALTER TABLE helper now enforces an allowlist and a strict
    identifier regex so a future caller can't turn it into an injection
    vector. connections.sandboxPassword / .description and
    oauth_refresh_tokens.familyId go through the allowlist.

claude added 2 commits April 20, 2026 12:23
Audits the self-hosted, multi-user, internet-facing deployment profile against
April 2026 OAuth BCP (RFC 9700), OAuth 2.1, and current secure-defaults.

Sandbox credentials (C1)
  * Every sandbox container now boots with a random AES-256-GCM-encrypted
    password persisted to connections.sandboxPassword before container
    creation, so a crash cannot leave a recoverable row that still grants
    access with the old 'askdb/askdb' literal.
  * Sandbox ports bind to 127.0.0.1 by default; SANDBOX_BIND_HOST env lets
    docker-in-docker deployments override when the server itself runs as a
    peer container and talks over host-gateway.
  * Legacy rows without a stored password keep working against the old
    container via a documented fallback until re-sync recreates them.

Adapter boundary (H1, M3)
  * assertValidDatabaseName('^[A-Za-z0-9_-]{1,63}$') gates all paths that
    interpolate a database name into shell args, URL pathnames, or quoted
    identifiers. Both mongodump/mongorestore and pg_dump/psql callers
    validate, and CREATE DATABASE drops its ad-hoc quote escaping now that
    the upstream validator rejects quotes entirely.
  * pg and mongo CLI stderr is scrubbed for URI userinfo and password= pairs
    before it lands in connections.sync_error.

OAuth (H2, M2, L2)
  * Redirect URI validation now matches RFC 9700 §4.1.3 and OAuth 2.1 §4.1.2:
    rejects fragments, userinfo, wildcards, oversize URIs, plain-http except
    loopback, and https pointing at RFC1918/link-local/CGNAT/metadata/ULA.
  * Refresh tokens rotate with a familyId and reuse-detection: presenting
    an already-revoked refresh token revokes the entire family AND all
    outstanding access tokens for that user+client pair, matching OAuth 2.1
    §4.2.2.
  * Default refresh-token TTL shortened from 30d to 7d.
  * New auth_audit_logs table records sign-in/sign-up outcomes, OAuth
    client registrations/updates, and refresh-token-reuse detections with
    IP/UA/timestamps for forensics.

Server transport (H4, M1)
  * helmet 8.1.0 applied with a tight CSP (self + inline styles only), 2y
    HSTS with includeSubDomains+preload, no-referrer, and COOP/CORP
    same-origin. First-party UI only; no third-party iframes.
  * /api/auth/sign-in and /api/auth/sign-up get a 5-req/15min limiter with
    skipSuccessfulRequests so legit credential-correct users aren't locked
    out mid-spray. draft-8 RateLimit-* headers only.

Schema migrations
  * Idempotent ALTER TABLE helper now enforces an allowlist and a strict
    identifier regex so a future caller can't turn it into an injection
    vector. connections.sandboxPassword / .description and
    oauth_refresh_tokens.familyId go through the allowlist.
install.sh now verifies every file it ingests (docker-compose.yml and
deploy/Caddyfile) against a SHA256SUMS manifest whose detached GPG signature
is checked against a release key fingerprint pinned in the script itself.

Verification is gated on:
  * ASKDB_VERSION matching vX.Y.Z (main/branch installs are unpinned anyway);
  * gpg being available on the installer host;
  * the pinned fingerprint actually having been filled in.
Any failure aborts the install; an explicit ASKDB_SKIP_VERIFY=1 escape hatch
bypasses verification with a loud warning for emergency re-installs.

Pubkey retrieval goes through hkps://keys.openpgp.org into a scratch
GNUPGHOME so we never touch the caller's keyring, and we re-assert the
received key's fingerprint matches the pinned one before trusting any
signature (keyservers are not authoritative).

release.yml now builds release-artifacts/ with a matching on-disk layout
(install scripts at root, docker-compose.yml at root, deploy/Caddyfile under
deploy/) so SHA256SUMS paths line up with what install.sh writes. It
generates SHA256SUMS, signs it with ASKDB_RELEASE_GPG_PRIVATE_KEY inside an
isolated GPG homedir, self-verifies the signature before publishing, and
uploads SHA256SUMS + SHA256SUMS.asc alongside the install scripts and the
compose + Caddyfile as release assets.

Maintainer action required: generate an askdb release key, add
ASKDB_RELEASE_GPG_PRIVATE_KEY + ASKDB_RELEASE_GPG_PASSPHRASE to repo
secrets, and replace REPLACE_WITH_RELEASE_KEY_FINGERPRINT in install.sh
with the real fingerprint before cutting the next release.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
askdb ef6b18e Apr 20 2026, 12:27 PM

@mgorabbani mgorabbani merged commit a6832f9 into main Apr 20, 2026
2 of 3 checks passed
@mgorabbani mgorabbani deleted the claude/security-audit-15WxT branch April 20, 2026 13:39
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.

2 participants