security: harden server for public-internet multi-tenant deployment#10
Merged
Conversation
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.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
askdb | ef6b18e | Apr 20 2026, 12:27 PM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
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.
docker-in-docker deployments override when the server itself runs as a
peer container and talks over host-gateway.
container via a documented fallback until re-sync recreates them.
Adapter boundary (H1, M3)
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.
before it lands in connections.sync_error.
OAuth (H2, M2, L2)
rejects fragments, userinfo, wildcards, oversize URIs, plain-http except
loopback, and https pointing at RFC1918/link-local/CGNAT/metadata/ULA.
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.
client registrations/updates, and refresh-token-reuse detections with
IP/UA/timestamps for forensics.
Server transport (H4, M1)
HSTS with includeSubDomains+preload, no-referrer, and COOP/CORP
same-origin. First-party UI only; no third-party iframes.
skipSuccessfulRequests so legit credential-correct users aren't locked
out mid-spray. draft-8 RateLimit-* headers only.
Schema migrations
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.