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
This commit is contained in:
@@ -158,6 +158,13 @@ func (db *DB) migrate() error {
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rpm_metadata_repo ON rpm_metadata(repo_name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS signing_keys (
|
||||
purpose TEXT PRIMARY KEY,
|
||||
private_key_armor TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package database
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSigningKeyRoundTripAndIdempotency(t *testing.T) {
|
||||
requireDB(t)
|
||||
|
||||
const purpose = "terraform-provider-test"
|
||||
|
||||
// Absent to start.
|
||||
if _, _, found, err := testDB.GetSigningKey(ctx(), purpose); err != nil || found {
|
||||
t.Fatalf("expected no key, got found=%v err=%v", found, err)
|
||||
}
|
||||
|
||||
if err := testDB.InsertSigningKeyIfAbsent(ctx(), purpose, "ARMOR-1", "KEYID1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A second insert must not overwrite (models the replica race).
|
||||
if err := testDB.InsertSigningKeyIfAbsent(ctx(), purpose, "ARMOR-2", "KEYID2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
armor, keyID, found, err := testDB.GetSigningKey(ctx(), purpose)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("expected key, found=%v err=%v", found, err)
|
||||
}
|
||||
if armor != "ARMOR-1" || keyID != "KEYID1" {
|
||||
t.Errorf("key was overwritten: armor=%q key_id=%q", armor, keyID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user