[object Object]

← back to Homesonspec

admin: fail-closed auth + generated prod password (TK-10 security #2, partial)

89f29a3a153107bb81baba081538658a16446f3d · 2026-07-27 17:10:19 -0700 · Steve Abrams

- middleware: remove hardcoded 'admin:DWSecure2024!' fallback; deny all when
  BASIC_AUTH is unset/malformed (fail closed). First-colon split on BOTH the
  configured value and the incoming credential (symmetric; ':' allowed in pw).
- deploy-kamatera: generate a random BASIC_AUTH into the gitignored .env.kamatera
  (mirrors the SPECHOMES_DB_PW pattern) instead of writing a committed default.
- .env.example: placeholder, not a real credential.

Verified: admin typecheck+build clean; gate 401(no-auth)/200(correct)/401(wrong).
NOT fully resolved (Steve/gated): the string still exists in tests/end-to-end/
admin.spec.ts AND git history — needs the same rotate+filter-repo scrub as the
GoDaddy key (TK-10 blocker A). Existing prod boxes rotate on next deploy-kamatera.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 89f29a3a153107bb81baba081538658a16446f3d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 17:10:19 2026 -0700

    admin: fail-closed auth + generated prod password (TK-10 security #2, partial)
    
    - middleware: remove hardcoded 'admin:DWSecure2024!' fallback; deny all when
      BASIC_AUTH is unset/malformed (fail closed). First-colon split on BOTH the
      configured value and the incoming credential (symmetric; ':' allowed in pw).
    - deploy-kamatera: generate a random BASIC_AUTH into the gitignored .env.kamatera
      (mirrors the SPECHOMES_DB_PW pattern) instead of writing a committed default.
    - .env.example: placeholder, not a real credential.
    
    Verified: admin typecheck+build clean; gate 401(no-auth)/200(correct)/401(wrong).
    NOT fully resolved (Steve/gated): the string still exists in tests/end-to-end/
    admin.spec.ts AND git history — needs the same rotate+filter-repo scrub as the
    GoDaddy key (TK-10 blocker A). Existing prod boxes rotate on next deploy-kamatera.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .env.example                 |  4 ++--
 apps/admin/src/middleware.ts | 26 ++++++++++++++++++++------
 deploy-kamatera.sh           |  9 ++++++++-
 3 files changed, 30 insertions(+), 9 deletions(-)

diff --git a/.env.example b/.env.example
index 9d771e7..e94bc1e 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,7 @@
 # Copy to .env — never commit real values.
 DATABASE_URL="postgresql://macstudio3@localhost/spechomes?host=/tmp"
 DATABASE_URL_TEST="postgresql://macstudio3@localhost/spechomes_test?host=/tmp"
-# Admin app HTTP Basic Auth (user:pass)
-BASIC_AUTH="admin:REDACTED_ADMIN_PW"
+# Admin app HTTP Basic Auth (user:pass) — set a strong value; admin fails CLOSED if unset.
+BASIC_AUTH="admin:CHANGE_ME_BEFORE_DEPLOY"
 # Snapshot storage root (raw fetched bodies live on disk, not in PG)
 SNAPSHOT_DIR="./var/snapshots"
diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts
index a263729..5053cd4 100644
--- a/apps/admin/src/middleware.ts
+++ b/apps/admin/src/middleware.ts
@@ -2,15 +2,29 @@ import { NextRequest, NextResponse } from "next/server";
 
 /**
  * HTTP Basic Auth for the whole admin app (fleet next-auth-gate convention).
- * Credentials from BASIC_AUTH env ("user:pass").
+ * Credentials come ONLY from the BASIC_AUTH env ("user:pass") — there is NO
+ * hardcoded fallback. If BASIC_AUTH is unset/malformed the admin fails CLOSED
+ * (denies every request) rather than accepting a committed default credential
+ * (TK-10 security remediation).
  */
-const [USER, PASS] = (process.env.BASIC_AUTH ?? "admin:REDACTED_ADMIN_PW").split(":");
+const RAW = process.env.BASIC_AUTH ?? "";
+const SEP = RAW.indexOf(":");
+const USER = SEP >= 0 ? RAW.slice(0, SEP) : "";
+const PASS = SEP >= 0 ? RAW.slice(SEP + 1) : "";
+const CONFIGURED = USER.length > 0 && PASS.length > 0;
 
 export function middleware(request: NextRequest) {
-  const header = request.headers.get("authorization");
-  if (header?.startsWith("Basic ")) {
-    const [user, pass] = Buffer.from(header.slice(6), "base64").toString().split(":");
-    if (user === USER && pass === PASS) return NextResponse.next();
+  if (CONFIGURED) {
+    const header = request.headers.get("authorization");
+    if (header?.startsWith("Basic ")) {
+      // Split on the FIRST colon only — symmetric with USER/PASS parsing above,
+      // so passwords may contain ':'.
+      const decoded = Buffer.from(header.slice(6), "base64").toString();
+      const sep = decoded.indexOf(":");
+      const user = sep >= 0 ? decoded.slice(0, sep) : decoded;
+      const pass = sep >= 0 ? decoded.slice(sep + 1) : "";
+      if (user === USER && pass === PASS) return NextResponse.next();
+    }
   }
   return new NextResponse("Authentication required", {
     status: 401,
diff --git a/deploy-kamatera.sh b/deploy-kamatera.sh
index 78fea5e..57140ba 100755
--- a/deploy-kamatera.sh
+++ b/deploy-kamatera.sh
@@ -13,6 +13,13 @@ if ! grep -q '^SPECHOMES_DB_PW=' "$DBPW_FILE" 2>/dev/null; then
   echo "SPECHOMES_DB_PW=$(openssl rand -hex 16)" >> "$DBPW_FILE"; chmod 600 "$DBPW_FILE"
 fi
 DBPW=$(grep '^SPECHOMES_DB_PW=' "$DBPW_FILE" | tail -1 | cut -d= -f2)
+# Admin Basic-Auth — generated once and stored in the gitignored .env.kamatera
+# (no committed default credential; TK-10 security remediation). Read the value
+# from that file to log into the admin. Rotate by deleting the line + redeploying.
+if ! grep -q '^SPECHOMES_ADMIN_AUTH=' "$DBPW_FILE" 2>/dev/null; then
+  echo "SPECHOMES_ADMIN_AUTH=admin:$(openssl rand -hex 16)" >> "$DBPW_FILE"; chmod 600 "$DBPW_FILE"
+fi
+ADMIN_AUTH=$(grep '^SPECHOMES_ADMIN_AUTH=' "$DBPW_FILE" | tail -1 | cut -d= -f2-)
 ssh $KH "sudo -u postgres psql -v ON_ERROR_STOP=1 -c \"DO \\\$\\\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='spechomes') THEN CREATE ROLE spechomes LOGIN PASSWORD '$DBPW'; ELSE ALTER ROLE spechomes PASSWORD '$DBPW'; END IF; END \\\$\\\$;\"; \
   sudo -u postgres createdb -O spechomes spechomes 2>/dev/null || echo '  db exists'; \
   sudo -u postgres psql -d spechomes -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;' >/dev/null"
@@ -34,7 +41,7 @@ rsync -az --delete \
 echo "──────── 4/6  Remote env + install + build ────────"
 ssh $KH "cat > $REMOTE/.env <<EOF
 DATABASE_URL=postgresql://spechomes:$DBPW@localhost/spechomes
-BASIC_AUTH=admin:REDACTED_ADMIN_PW
+BASIC_AUTH=$ADMIN_AUTH
 SNAPSHOT_DIR=$REMOTE/var/snapshots
 NODE_ENV=production
 EOF

← 2fa9a7f auto-save: 2026-07-27T16:52:13 (3 files) — apps/workers/src/  ·  back to Homesonspec  ·  auto-save: 2026-07-27T17:22:24 (1 files) — deploy-kamatera.s 77fbb63 →