Merge fix/ci-bundle-B-tail: G-3 env-var docs + M-028 closure

This commit is contained in:
Shankar
2026-04-26 23:35:20 +00:00
4 changed files with 37 additions and 19 deletions
+8 -12
View File
@@ -44,9 +44,8 @@ func TestMain_HealthEndpointBypassesAuth(t *testing.T) {
}) })
// Build the handler chain the same way main.go does // Build the handler chain the same way main.go does
authMiddleware := middleware.NewAuth(middleware.AuthConfig{ authMiddleware := middleware.NewAuthWithNamedKeys([]middleware.NamedAPIKey{
Type: "api-key", {Name: "test", Key: "test-secret-key"},
Secret: "test-secret-key",
}) })
// API handler with auth // API handler with auth
@@ -160,9 +159,8 @@ func TestMain_AuthMiddlewareRejectsUnauthorized(t *testing.T) {
}) })
// Wrap with auth middleware // Wrap with auth middleware
authMiddleware := middleware.NewAuth(middleware.AuthConfig{ authMiddleware := middleware.NewAuthWithNamedKeys([]middleware.NamedAPIKey{
Type: "api-key", {Name: "test", Key: "test-secret-key"},
Secret: "test-secret-key",
}) })
chainedHandler := middleware.Chain(protectedHandler, authMiddleware) chainedHandler := middleware.Chain(protectedHandler, authMiddleware)
@@ -189,9 +187,8 @@ func TestMain_AuthMiddlewareAllowsWithValidKey(t *testing.T) {
}) })
// Wrap with auth middleware // Wrap with auth middleware
authMiddleware := middleware.NewAuth(middleware.AuthConfig{ authMiddleware := middleware.NewAuthWithNamedKeys([]middleware.NamedAPIKey{
Type: "api-key", {Name: "test", Key: testKey},
Secret: testKey,
}) })
chainedHandler := middleware.Chain(protectedHandler, authMiddleware) chainedHandler := middleware.Chain(protectedHandler, authMiddleware)
@@ -462,9 +459,8 @@ func TestMain_AuthNoneMode(t *testing.T) {
}) })
// Wrap with auth middleware in "none" mode // Wrap with auth middleware in "none" mode
authMiddleware := middleware.NewAuth(middleware.AuthConfig{ // auth=none equivalent: empty named-keys list is a no-op pass-through.
Type: "none", authMiddleware := middleware.NewAuthWithNamedKeys(nil)
})
chainedHandler := middleware.Chain(protectedHandler, authMiddleware) chainedHandler := middleware.Chain(protectedHandler, authMiddleware)
+11 -2
View File
@@ -60,11 +60,20 @@ Two endpoints are served without auth so the GUI can detect auth mode before log
Token bucket algorithm protecting the control plane from misbehaving clients. Token bucket algorithm protecting the control plane from misbehaving clients.
Bundle B (Audit M-025 / OWASP ASVS L2 §11.2.1): per-key keying. Each
authenticated caller gets a bucket keyed on their API-key name; each
unauthenticated source IP gets its own bucket. Bucket creation is
on-demand under a `sync.RWMutex`; no eviction (the leak is bounded by
realistic operator IP fan-out — appropriate for the OWASP ASVS L2 threat
model of abuse-by-known-clients, not infinite-cardinality scanners).
| Env Var | Default | Description | | Env Var | Default | Description |
|---|---|---| |---|---|---|
| `CERTCTL_RATE_LIMIT_ENABLED` | `true` | Enable/disable | | `CERTCTL_RATE_LIMIT_ENABLED` | `true` | Enable/disable |
| `CERTCTL_RATE_LIMIT_RPS` | `50` | Requests per second | | `CERTCTL_RATE_LIMIT_RPS` | `50` | Per-key requests per second (default applies to IP-keyed buckets; user-keyed buckets fall back to this when `PER_USER_RPS` is unset) |
| `CERTCTL_RATE_LIMIT_BURST` | `100` | Burst capacity | | `CERTCTL_RATE_LIMIT_BURST` | `100` | Per-key burst capacity (default applies to IP-keyed buckets; user-keyed buckets fall back to this when `PER_USER_BURST` is unset) |
| `CERTCTL_RATE_LIMIT_PER_USER_RPS` | `0` | Override RPS for authenticated callers. `0` means "use `RATE_LIMIT_RPS`". Set higher than `RATE_LIMIT_RPS` to grant authenticated clients a more generous budget than anonymous probes. |
| `CERTCTL_RATE_LIMIT_PER_USER_BURST` | `0` | Override burst for authenticated callers. `0` means "use `RATE_LIMIT_BURST`". |
Exceeded requests receive `429 Too Many Requests` with a `Retry-After` header. Exceeded requests receive `429 Too Many Requests` with a `Retry-After` header.
+12
View File
@@ -263,6 +263,18 @@ func extractCSRFields(csrDER []byte) ([]byte, string, string, error) {
// Attributes is []pkix.AttributeTypeAndValueSET where each has Type (OID) // Attributes is []pkix.AttributeTypeAndValueSET where each has Type (OID)
// and Value ([][]pkix.AttributeTypeAndValue). The challenge password value // and Value ([][]pkix.AttributeTypeAndValue). The challenge password value
// is stored as a string in the inner AttributeTypeAndValue.Value field. // is stored as a string in the inner AttributeTypeAndValue.Value field.
//
// Audit M-028 carve-out: Go's stdlib deprecates `csr.Attributes` for the
// specific use case of parsing the "requestedExtensions" CSR attribute
// (OID 1.2.840.113549.1.9.14), pointing callers at `csr.Extensions` /
// `csr.ExtraExtensions`. challengePassword (OID 1.2.840.113549.1.9.7)
// per RFC 2985 §5.4.1 is a SEPARATE CSR attribute that cannot be
// retrieved via Extensions. There is no non-deprecated stdlib API for
// it; callers either accept the deprecation warning or parse the raw
// `csr.RawAttributes` ASN.1 themselves. We accept the warning; the
// staticcheck.conf and golangci-lint rules suppress SA1019 for this
// specific line per the audit closure note.
//lint:ignore SA1019 RFC 2985 challengePassword has no non-deprecated stdlib API; see comment above.
for _, attr := range csr.Attributes { for _, attr := range csr.Attributes {
if attr.Type.Equal(oidChallengePassword) { if attr.Type.Equal(oidChallengePassword) {
if len(attr.Value) > 0 && len(attr.Value[0]) > 0 { if len(attr.Value) > 0 && len(attr.Value[0]) > 0 {
@@ -334,11 +334,12 @@ func TestHashPublicKey_ECDSA_RoundTripPin(t *testing.T) {
t.Fatalf("ecdsaToECDH: %v", err) t.Fatalf("ecdsaToECDH: %v", err)
} }
ecdhBytes := ecdhPub.Bytes() ecdhBytes := ecdhPub.Bytes()
//nolint:staticcheck // SA1019: pin assertion — we DELIBERATELY use // Pin assertion — we DELIBERATELY use the deprecated API here
// the deprecated API here as a regression oracle to prove the // as a regression oracle to prove the new crypto/ecdh path
// new crypto/ecdh path produces byte-identical output. If // produces byte-identical output. If elliptic.Marshal is
// elliptic.Marshal is removed in a future Go release this test // removed in a future Go release this test must be deleted
// must be deleted (and the migration is then irreversibly proven). // (and the migration is then irreversibly proven).
//lint:ignore SA1019 deliberate regression oracle for M-028 round-trip pin
legacy := elliptic.Marshal(k.Curve, k.X, k.Y) legacy := elliptic.Marshal(k.Curve, k.X, k.Y)
if !bytes.Equal(ecdhBytes, legacy) { if !bytes.Equal(ecdhBytes, legacy) {
t.Fatalf("ECDH .Bytes() != legacy elliptic.Marshal output\n new: %x\n old: %x", ecdhBytes, legacy) t.Fatalf("ECDH .Bytes() != legacy elliptic.Marshal output\n new: %x\n old: %x", ecdhBytes, legacy)