mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 22:21:30 +00:00
30f9f1e712
Closes M-001 + M-002 + M-013 + M-018 + M-025 from
comprehensive-audit-2026-04-25.
M-001 (CWE-916) — PBKDF2 100k -> 600k via v3 blob format
internal/crypto/encryption.go:
- New v3Magic (0x03), pbkdf2IterationsV3 (600,000 — OWASP 2024
Password Storage Cheat Sheet floor), v3SaltSize (16 bytes),
deriveKeyWithSaltV3 helper.
- EncryptIfKeySet now unconditionally writes v3:
magic(0x03) || salt(16) || nonce(12) || ciphertext+tag
- DecryptIfKeySet falls through v3 -> v2 -> v1 with AEAD verification
at each step. Wrong-passphrase v3 reads cannot be silently
misattributed to v2/v1.
- IsLegacyFormat updated to recognize 0x03 as non-legacy.
internal/crypto/encryption_v3_test.go (NEW, 7 tests):
V3 round-trip / V2 read-fallback against deterministic v2 fixture /
V3 wrong-passphrase fails / V3-vs-V2 dispatch order / V2 vs V3 keys
differ for same (passphrase, salt) / iteration-count pin at OWASP
2024 floor / IsLegacyFormat-recognises-V3.
Coverage internal/crypto: 86.7% -> 88.2%.
M-002 (CWE-862) — Auth-exempt allowlist constants + AST regression test
Recon found auth-exempt surface spans TWO layers (audit's claim was
incomplete):
Layer 1 (router.go direct r.mux.Handle):
GET /health, GET /ready, GET /api/v1/auth/info, GET /api/v1/version
Layer 2 (cmd/server/main.go::buildFinalHandler URL-prefix dispatch):
/.well-known/pki/*, /.well-known/est/*, /scep[/...]*
internal/api/router/router.go:
- New AuthExemptRouterRoutes constant with per-entry justifications.
- New AuthExemptDispatchPrefixes constant.
internal/api/router/auth_exempt_test.go (NEW, 2 tests):
AST-walks router.go for every direct mux.Handle call and asserts
set equals AuthExemptRouterRoutes; reads source bytes of Register /
RegisterFunc and asserts they still wrap with middleware.Chain.
cmd/server/auth_exempt_test.go (NEW, 2 tests):
14-case table test on buildFinalHandler asserting documented
prefixes route to noAuthHandler and authenticated routes route to
apiHandler; inverse-overlap pin proves no documented bypass shadows
an authenticated prefix.
M-013 (CWE-942) — CORS deny-by-default verified-already-clean + pin
Audit claim 'default allows all origins if env-var unset' was WRONG.
internal/api/middleware/middleware.go::NewCORS already denies cross-
origin requests when len(cfg.AllowedOrigins) == 0 (no
Access-Control-Allow-Origin header is emitted, same-origin policy
applies).
internal/api/middleware/cors_test.go: +TestNewCORS_NilOriginsDeniesAll
+ TestNewCORS_M013_ContractDocumentedInOrder (5-case table test
pinning the 3-arm dispatch contract).
M-018 (CWE-319 / PCI-DSS Req 4) — Postgres TLS opt-in toggle
deploy/helm/certctl/values.yaml: new postgresql.tls.{mode,caSecretRef}
operator-facing knobs. Default 'disable' preserves in-cluster pod-
network behavior; PCI-scoped operators set verify-full.
deploy/helm/certctl/templates/_helpers.tpl: certctl.databaseURL helper
pipes postgresql.tls.mode into ?sslmode=.
deploy/helm/certctl/templates/server-secret.yaml: uses the helper
instead of hardcoded sslmode=disable.
deploy/docker-compose.yml: CERTCTL_DATABASE_URL is now
${CERTCTL_DATABASE_URL:-...} so operators override without editing.
docs/database-tls.md (NEW): operator runbook covering 4 deployment
shapes, RDS verify-full example with PGSSLROOTCERT mount, and
pg_stat_ssl verification query.
helm template + helm lint clean.
M-025 (OWASP ASVS L2 §11.2.1) — Per-key rate limiting
internal/api/middleware/middleware.go::NewRateLimiter rewritten from
a single global tokenBucket to a keyedRateLimiter map keyed on
'user:'+GetUser(ctx) for authenticated callers
'ip:'+RemoteAddr-host for unauthenticated
- Empty UserKey strings treated as unauthenticated.
- X-Forwarded-For intentionally NOT consulted (header-spoofing risk).
- Create-on-demand bucket allocation under sync.RWMutex with double-
check pattern.
RateLimitConfig.PerUserRPS / PerUserBurstSize fields with env vars
CERTCTL_RATE_LIMIT_PER_USER_RPS / CERTCTL_RATE_LIMIT_PER_USER_BURST
allow per-user budgets distinct from per-IP.
internal/api/middleware/ratelimit_keyed_test.go (NEW, 5 tests):
TwoIPsHaveIndependentBuckets / SameUserDifferentIPsShareBucket /
TwoUsersHaveIndependentBuckets / PerUserBudgetOverride /
EmptyUserKeyTreatedAsAnonymous.
Coverage internal/api/middleware: 82.1% -> 83.7%.
Audit deliverables:
cowork/comprehensive-audit-2026-04-25/audit-report.md: score
25/55 -> 30/55 closed (High 7/9, Medium 7/27 -> 12/27, Low 8/19).
cowork/comprehensive-audit-2026-04-25/findings.yaml: 5 status flips
open -> closed with closure notes citing the Bundle B mechanism.
certctl/CHANGELOG.md: Bundle B section under [unreleased].
Verification:
go test -count=1 -short ./... all green
staticcheck on changed packages no new SA*/ST* hits
(the 4 pre-existing SA1019 sites in cmd/server/main_test.go are
Bundle 9 / M-028 partial closure leftovers tracked in Bundle C)
helm template + helm lint clean
internal/repository/postgres setup-fail sandbox disk pressure,
same on master HEAD before this branch — environmental, not Bundle B
118 lines
5.1 KiB
Markdown
118 lines
5.1 KiB
Markdown
# Database TLS — Postgres Transport Encryption
|
|
|
|
**Audit reference:** Bundle B / M-018. PCI-DSS v4.0 Req 4 §2.2.5; CWE-319.
|
|
|
|
certctl talks to Postgres over a single connection-string URL controlled by the
|
|
`CERTCTL_DATABASE_URL` env var. The `sslmode` query parameter on that URL
|
|
selects the transport-encryption posture. Pre-Bundle-B all the bundled
|
|
deployment artifacts (Helm chart, docker-compose) hard-coded `sslmode=disable`.
|
|
Bundle B exposes that as an operator-facing knob with a documented default and
|
|
explicit opt-in / opt-out paths for the four real-world deployment shapes.
|
|
|
|
## Quick reference
|
|
|
|
| Deployment shape | Default `sslmode` | When to change |
|
|
|------------------------------------------------|--------------------|----------------|
|
|
| Helm chart, bundled Postgres, in-cluster | `disable` | When the cluster does not provide pod-network encryption (CNI without WireGuard / IPSec) and the workload is in PCI-DSS scope. |
|
|
| Helm chart, external Postgres (RDS / Cloud SQL / Azure DB) | not auto-set | **Always** set to `verify-full` and provide the cloud provider's server CA bundle. |
|
|
| docker-compose, bundled Postgres on docker bridge | `disable` | Demo/dev only; not a deployment shape we expect operators to harden. |
|
|
| docker-compose / k8s with external Postgres | not auto-set | **Always** set `CERTCTL_DATABASE_URL` to a connection string with `sslmode=verify-full`. |
|
|
|
|
`sslmode` values come from `lib/pq` (the underlying driver). The full set is:
|
|
`disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full`. PCI-DSS
|
|
Req 4 v4.0 §2.2.5 considers `verify-ca` the floor for sensitive-data transport;
|
|
`verify-full` is the floor for systems exposed to spoofing risk (it adds
|
|
hostname validation against the server cert's CN/SAN).
|
|
|
|
## Helm chart (Bundle B)
|
|
|
|
Bundle B adds two values under `postgresql.tls`:
|
|
|
|
```yaml
|
|
postgresql:
|
|
tls:
|
|
mode: disable # disable | require | verify-ca | verify-full
|
|
caSecretRef: "" # Secret with ca.crt key (required for verify-ca / verify-full)
|
|
```
|
|
|
|
The chart pipes `postgresql.tls.mode` into the `?sslmode=` parameter of the
|
|
generated `CERTCTL_DATABASE_URL` (see `templates/_helpers.tpl::certctl.databaseURL`).
|
|
For external Postgres, set `postgresql.enabled: false` and override
|
|
`server.env.CERTCTL_DATABASE_URL` directly with the full connection string —
|
|
the operator authoring an external-DB values file owns the entire URL.
|
|
|
|
### Example: external RDS with verify-full
|
|
|
|
```yaml
|
|
postgresql:
|
|
enabled: false # Disable bundled Postgres
|
|
|
|
server:
|
|
env:
|
|
CERTCTL_DATABASE_URL: |
|
|
postgres://certctl:STRONGPW@my-db.cabc12345.us-east-1.rds.amazonaws.com:5432/certctl?sslmode=verify-full
|
|
|
|
# Provide the AWS RDS root CA bundle as a secret + mount.
|
|
# AWS publishes per-region root certs at https://truststore.pki.rds.amazonaws.com/
|
|
extraVolumes:
|
|
- name: rds-ca
|
|
secret:
|
|
secretName: rds-ca-bundle # kubectl create secret generic rds-ca-bundle --from-file=ca.crt=...
|
|
|
|
extraVolumeMounts:
|
|
- name: rds-ca
|
|
mountPath: /etc/postgresql-ca
|
|
readOnly: true
|
|
|
|
# lib/pq honors PGSSLROOTCERT for the verify-{ca,full} CA bundle path.
|
|
server:
|
|
env:
|
|
PGSSLROOTCERT: /etc/postgresql-ca/ca.crt
|
|
```
|
|
|
|
## docker-compose (development / demo)
|
|
|
|
The bundled `deploy/docker-compose.yml` keeps `sslmode=disable` as the default
|
|
because the Postgres container shares the docker bridge network with the certctl
|
|
server and the compose file is not a production deployment artifact. To opt in:
|
|
|
|
```bash
|
|
export CERTCTL_DATABASE_URL='postgres://certctl:certctl@postgres:5432/certctl?sslmode=verify-full'
|
|
docker compose up
|
|
```
|
|
|
|
## Verification
|
|
|
|
For any non-`disable` mode, confirm the connection actually negotiated TLS:
|
|
|
|
```bash
|
|
# From inside the certctl-server container or any host with psql + the same URL:
|
|
psql "$CERTCTL_DATABASE_URL" -c "SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
|
|
|
|
# Expected output for verify-full: ssl=t, version=TLSv1.3 (or TLSv1.2), cipher=...
|
|
```
|
|
|
|
If `ssl=f` appears, the connection silently fell back to plaintext — investigate
|
|
the cert chain or sslmode value before treating the deployment as PCI-compliant.
|
|
|
|
## What this does NOT cover
|
|
|
|
* **Postgres-to-Postgres replication** — if you run a replica, replica-primary
|
|
TLS is configured via the Postgres server itself (`pg_hba.conf` +
|
|
`ssl=on`); it is independent of certctl's `CERTCTL_DATABASE_URL`.
|
|
* **Backup transport** — `pg_dump` / `pg_basebackup` honor the same `sslmode`
|
|
parameter when invoked with the URL form, but the bundled chart's backup
|
|
story (if any) is operator-owned.
|
|
* **Encryption at rest** — `sslmode` is a transport concern only. Disk
|
|
encryption is the cloud provider's storage layer (RDS, EBS, etc.) or the
|
|
operator's Postgres TDE / disk LUKS / etc.
|
|
|
|
## Reverting
|
|
|
|
If `sslmode=verify-full` causes connection failures (most common: missing CA
|
|
bundle, wrong hostname), drop temporarily to `sslmode=require` to confirm TLS
|
|
is at least negotiated, then add the CA bundle and ratchet back up. Never
|
|
revert to `sslmode=disable` on a system carrying real cert metadata —
|
|
audit_events alone contains enough operator/issuer/target identity to justify
|
|
TLS in any scoped environment.
|