[Security Chore]: Gateway signing-key protection #1940
Chore: Gateway signing-key protection
Status: Deferred. Not part of PR #1568.
Related: PR #1568 review on gateway_devshards.private_key_hex. Earlier SQLite note: escrow-keys-at-rest.md.
This is an issue description. It does not change the Postgres gateway store.
Decision for PR #1568
Storing private_key_hex in Postgres is acceptable for this PR. The database is the operator's, and protecting it is the operator's responsibility: who can read it, backups, replicas, and the path between the gateway process and Postgres. That matches the previous SQLite gateway store.
private_key_env stays as the alternative. When the hex column is empty and the env name is set, the gateway reads the key from the process environment and the database holds only the variable name.
Forcing TLS, or refusing to persist hex, is a deployment policy. It is out of scope for the Postgres backend change.
What the review asked
gateway_devshards keeps the devshard signing key in private_key_hex and the optional env name in private_key_env. In a multi-instance deployment every gateway that shares the database can read the key, as can backups, replicas, and any role with SELECT on that table.
The pool is opened with pgxpool.ParseConfig("") in gateway_store_postgres.go and accounting/store_postgres.go. That uses libpq defaults, including sslmode=prefer. prefer uses TLS when the server offers it, and otherwise connects in the clear. It does not check the server certificate. A SELECT of private_key_hex can therefore cross the network unencrypted, or toward a host that is only pretending to be Postgres, unless the operator sets PGSSLMODE=require or verify-full. ParseConfig already honors PGSSLMODE; the client does not require it.
On a Postgres that stays on the operator's own machine, that traffic does not leave the host.
Current behavior
- Create and import accept either an inline
private_keyorprivate_key_env. An inline key is stored as hex. An env-only request stores the variable name. - Rotation commitments store
private_key_envonly. GET /v1/admin/stateclearsprivate_keybefore writing the response. The env name is still returned.- Runtime startup uses the stored hex when it is set, and otherwise reads
os.Getenv(private_key_env).
Follow-up
Decide, as its own chore, whether gateway key handling should get stricter than "the operator protects the database":
- Document
private_key_envas the production path, and treat a populatedprivate_key_hexas an operator choice rather than the default for new escrows. - Document
PGSSLMODEfor any Postgres that is not on the gateway host.requireencrypts.verify-fullalso checks the server certificate and hostname. Apply the same note to the other pools that useParseConfig("")(session, payload, accounting, inference stats). - Optionally stop persisting hex once env or secret-store references cover create, import, and rotation. Encryption at rest (
DEVSHARD_GATEWAY_SECRETS_KEYor equivalent) remains the later option already sketched in escrow-keys-at-rest.md.
Out of scope
- Blocking PR #1568 on this.
- Per-escrow HSM or remote signing.
- Changing the pool size cap (
PG_POOL_MAX_CONNS). That is a separate connection-count limit and does not encrypt the session.
🔄 Auto-synced from Issue #1940 every hour.