tenseleyflow/shithub / abe12ad

Browse files

config + api.Deps: wire sealed-box keypair for actions secrets

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
abe12ad84f16a6b65ef9db7cdd232b64b3132bd1
Parents
71d3178
Tree
9c52ab1

3 changed files

StatusFile+-
M internal/infra/config/config.go 22 0
M internal/web/auth_wiring.go 22 0
M internal/web/handlers/api/api.go 7 0
internal/infra/config/config.gomodified
@@ -41,6 +41,7 @@ type Config struct {
4141
 	Notif          NotifConfig          `toml:"notif"`
4242
 	RateLimit      RateLimitConfig      `toml:"ratelimit"`
4343
 	Billing        BillingConfig        `toml:"billing"`
44
+	Actions        ActionsConfig        `toml:"actions"`
4445
 }
4546
 
4647
 // RateLimitConfig configures runtime rate-limit budgets for surfaces that
@@ -62,6 +63,27 @@ type APIRateLimitConfig struct {
6263
 	AnonPerHour   int `toml:"anon_per_hour"`
6364
 }
6465
 
66
+// ActionsConfig groups configuration for the Actions subsystem. Today
67
+// it carries only the sealed-box keypair used by the REST secrets
68
+// surface; secrets storage encryption (at-rest) is handled by the
69
+// shared `auth.totp_key_b64` AEAD key.
70
+type ActionsConfig struct {
71
+	Secrets ActionsSecretsConfig `toml:"secrets"`
72
+}
73
+
74
+// ActionsSecretsConfig configures the NaCl sealed-box keypair the
75
+// REST `actions/secrets/public-key` endpoint serves. Clients encrypt
76
+// secret values against this public key; the server decrypts on PUT
77
+// before re-encrypting with the storage AEAD.
78
+//
79
+// BoxPrivateKeyB64 is base64 of a 32-byte X25519 private key. When
80
+// empty the server auto-generates a per-process keypair on startup
81
+// and logs a warning — secrets PUT against one process won't be
82
+// decryptable by another, so production deployments MUST set it.
83
+type ActionsSecretsConfig struct {
84
+	BoxPrivateKeyB64 string `toml:"box_private_key_b64"`
85
+}
86
+
6587
 // NotifConfig configures the S29 notification surface. UnsubscribeKeyB64
6688
 // is the base64-encoded HMAC-SHA256 key that signs one-click
6789
 // unsubscribe URLs (RFC 8058). When empty (dev default), the
internal/web/auth_wiring.gomodified
@@ -20,6 +20,7 @@ import (
2020
 	"github.com/tenseleyFlow/shithub/internal/auth/password"
2121
 	"github.com/tenseleyFlow/shithub/internal/auth/pat"
2222
 	"github.com/tenseleyFlow/shithub/internal/auth/runnerjwt"
23
+	"github.com/tenseleyFlow/shithub/internal/auth/sealbox"
2324
 	"github.com/tenseleyFlow/shithub/internal/auth/secretbox"
2425
 	"github.com/tenseleyFlow/shithub/internal/auth/session"
2526
 	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
@@ -67,6 +68,26 @@ func buildAPIHandlers(
6768
 			shithubdPath = abs
6869
 		}
6970
 	}
71
+	// X25519 sealed-box keypair for the REST `actions/secrets/public-key`
72
+	// endpoint. Loaded from config when present; auto-generated with a
73
+	// loud warning otherwise (dev convenience — production deployments
74
+	// MUST set the env knob so secrets survive process restart).
75
+	var secretsBox *sealbox.Box
76
+	if pk := cfg.Actions.Secrets.BoxPrivateKeyB64; pk != "" {
77
+		b, err := sealbox.FromBase64(pk)
78
+		if err != nil {
79
+			return nil, fmt.Errorf("api: actions secrets box: %w", err)
80
+		}
81
+		secretsBox = b
82
+	} else {
83
+		b, err := sealbox.New()
84
+		if err != nil {
85
+			return nil, fmt.Errorf("api: actions secrets box (auto): %w", err)
86
+		}
87
+		logger.Warn("actions secrets box: auto-generated keypair; secrets PUT against this process will not be decryptable after restart",
88
+			"hint", "set SHITHUB_ACTIONS__SECRETS__BOX_PRIVATE_KEY_B64=$(openssl rand -base64 32) for persistence")
89
+		secretsBox = b
90
+	}
7091
 	return apih.New(apih.Deps{
7192
 		Pool:         pool,
7293
 		Debouncer:    sharedPATDebouncer,
@@ -75,6 +96,7 @@ func buildAPIHandlers(
7596
 		RepoFS:       rfs,
7697
 		RunnerJWT:    runnerJWT,
7798
 		SecretBox:    secretBox,
99
+		SecretsBox:   secretsBox,
78100
 		RateLimiter:  rateLimiter,
79101
 		Audit:        audit.NewRecorder(),
80102
 		Throttle:     throttle.NewLimiter(),
internal/web/handlers/api/api.gomodified
@@ -21,6 +21,7 @@ import (
2121
 	"github.com/tenseleyFlow/shithub/internal/auth/audit"
2222
 	"github.com/tenseleyFlow/shithub/internal/auth/pat"
2323
 	"github.com/tenseleyFlow/shithub/internal/auth/runnerjwt"
24
+	"github.com/tenseleyFlow/shithub/internal/auth/sealbox"
2425
 	"github.com/tenseleyFlow/shithub/internal/auth/secretbox"
2526
 	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
2627
 	"github.com/tenseleyFlow/shithub/internal/infra/storage"
@@ -41,6 +42,12 @@ type Deps struct {
4142
 	RepoFS      *storage.RepoFS
4243
 	RunnerJWT   *runnerjwt.Signer
4344
 	SecretBox   *secretbox.Box
45
+	// SecretsBox holds the X25519 keypair used by the
46
+	// `/actions/secrets/public-key` endpoint to encrypt secret values
47
+	// in transit. Distinct from `SecretBox` (the symmetric at-rest
48
+	// AEAD key shared with the runner). Nil disables the secrets
49
+	// REST surface.
50
+	SecretsBox  *sealbox.Box
4451
 	RateLimiter *ratelimit.Limiter
4552
 	// Audit records security-sensitive mutations (repo create/delete,
4653
 	// settings changes). Required for any handler that mutates server