Files
artifactapi/internal/database/signing_keys.go
T
unkinben 97cdb9c6b5
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
feat: self-generate and store the terraform registry signing key
Rather than requiring an operator to create a GPG key and a K8s secret, the
registry now provisions itself: on first start artifactapi generates a signing
keypair and persists it in a new signing_keys table, so all replicas share one
key and there is nothing to set up. TF_SIGNING_KEY_PATH still overrides with a
bring-your-own key when set.

- signing_keys table + GetSigningKey / InsertSigningKeyIfAbsent (ON CONFLICT DO
  NOTHING so a replica race converges on one key)
- tfsign.Generate, LoadArmored, and LoadOrCreate(store, purpose)
- server prefers a configured key file, else LoadOrCreate against the DB
- tests: generate/load round-trip, load-or-create generates once then reuses,
  DB insert idempotency
2026-07-03 18:46:09 +10:00

36 lines
1.1 KiB
Go

package database
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
)
// GetSigningKey returns the stored armored private key and key id for a purpose.
// found is false when no key has been generated yet.
func (db *DB) GetSigningKey(ctx context.Context, purpose string) (armor, keyID string, found bool, err error) {
row := db.Pool.QueryRow(ctx, `
SELECT private_key_armor, key_id FROM signing_keys WHERE purpose = $1
`, purpose)
if err := row.Scan(&armor, &keyID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return "", "", false, nil
}
return "", "", false, err
}
return armor, keyID, true, nil
}
// InsertSigningKeyIfAbsent stores a freshly generated key, doing nothing if
// another replica already inserted one. Callers re-read with GetSigningKey to
// pick up whichever key won the race.
func (db *DB) InsertSigningKeyIfAbsent(ctx context.Context, purpose, armor, keyID string) error {
_, err := db.Pool.Exec(ctx, `
INSERT INTO signing_keys (purpose, private_key_armor, key_id)
VALUES ($1, $2, $3)
ON CONFLICT (purpose) DO NOTHING
`, purpose, armor, keyID)
return err
}