Skip to content

Fix/secret handling hardening - #991

Merged
andy-esch merged 4 commits into
mainfrom
fix/secret-handling-hardening
Sep 1, 2026
Merged

Fix/secret handling hardening#991
andy-esch merged 4 commits into
mainfrom
fix/secret-handling-hardening

Conversation

@andy-esch

Copy link
Copy Markdown
Owner

No description provided.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.70%. Comparing base (6c30e34) to head (fa3114e).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #991      +/-   ##
==========================================
+ Coverage   82.68%   82.70%   +0.02%     
==========================================
  Files         313      313              
  Lines       15809    15814       +5     
  Branches     3382     3208     -174     
==========================================
+ Hits        13071    13079       +8     
+ Misses       2544     2541       -3     
  Partials      194      194              
Flag Coverage Δ
go-apigateway 63.93% <ø> (ø)
go-apigateway-integration 91.63% <ø> (ø)
go-dispatcher 77.33% <ø> (ø)
go-firestore-integration 22.11% <ø> (ø)
go-shared 87.43% <100.00%> (+0.08%) ⬆️
python 93.87% <ø> (ø)
python-integration 44.47% <ø> (ø)
web 81.56% <ø> (+0.04%) ⬆️
web-integration 27.71% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/shared/secrets/secrets.go 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andy-esch
andy-esch force-pushed the fix/secret-handling-hardening branch from 064d6b8 to 8f777ea Compare September 1, 2026 11:08
Two ops scripts passed secrets as command-line arguments, where any co-tenant
can read them from `ps aux` / /proc/<pid>/cmdline for the life of the call.
Both violate docs/guides/secure-scripting.md §1 ("No Secrets in Args").

- webhook-management.sh `delete`: fed the request line over `curl --config -`
  (stdin) instead of argv, and switched the bare `curl -s ... | jq .` to a
  captured response with `--fail-with-body`, so a Strava 4xx/5xx now propagates
  a non-zero exit instead of being masked by jq's exit 0 under `set -o pipefail`.
  Partial by necessity: Strava's v3 API documents client_id/client_secret as
  REQUIRED query parameters for DELETE, with no request-body form, so the secret
  still reaches Strava's access logs. Only the local argv leak is closed; the
  residual is documented at the call site with a rotate-if-exposed note.
- connect.sh: parse the connection string and pass the password via PGPASSWORD
  rather than as a psql argument, reusing migrate.sh's existing sed shapes rather
  than a second parser. Added a guard that rejects a URI it cannot parse instead
  of silently connecting with empty credentials.

shellcheck clean on both.
…nil)

LoadFromMount treated a present-but-empty (or whitespace-only) mount file as a
successful load of "". That let a caller receive an empty string with a nil
error, indistinguishable from a real secret — and, worse, an empty mount
shadowed the documented env fallback, because it returned success before the
fallback could run. The apigateway would then go on to sign OAuth state tokens
with an empty HMAC key.

Treat an empty/whitespace mount as an absent secret: fall through to the env
fallback, and error if that is also empty — restoring the symmetry the env path
already had. Checked all six call sites first; every one loads a real credential
(Strava id/secret, auth state secret, Postgres conn, callback capability) and
none benefits from an empty value, so the stricter contract is safe everywhere.

The existing TestLoadFromMount_FileExistsEmpty asserted the old ("", nil)
behaviour — that assertion was the defect, so it is inverted (now expects an
error) rather than worked around, and a case is added proving an empty mount
falls back to a populated env var. Covers all five cases the finding named.

go test ./packages/shared/secrets/ green.
govulncheck (the gate added in the previous commit) flags 7 Go standard-library
vulnerabilities, every one fixed in go1.26.6: GO-2026-6089 (net/http H2C
ReadHeaderTimeout), GO-2026-6088 (encoding/xml recursion depth, reached via
pgx Scan), GO-2026-5972 (encoding/asn1 recursion), GO-2026-5026 (x/net/idna
punycode via net/http), and the others in that patch.

Because these are stdlib, no `require` bump fixes them — only the toolchain
does. Bumped in lockstep so CI, the workspace, and every module agree:
- .github/workflows/ci.yml  GO_VERSION 1.26.5 -> 1.26.6
- go.work + packages/{apigateway,dispatcher,shared}/go.mod  go 1.26.5 -> 1.26.6

Verified per-module from each module dir (as CI runs govulncheck):
apigateway / dispatcher / shared all report 0 vulnerabilities. go-test 0
failures, go-lint 0 issues, build clean.
@andy-esch
andy-esch force-pushed the fix/secret-handling-hardening branch from 8f777ea to fa3114e Compare September 1, 2026 11:30
@andy-esch
andy-esch merged commit 387e888 into main Sep 1, 2026
21 checks passed
@andy-esch
andy-esch deleted the fix/secret-handling-hardening branch September 1, 2026 11:52
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