🧪 test(bdd): admin metadata endpoint security property — no secret leak (#52)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #52.
This commit is contained in:
@@ -40,6 +40,16 @@ Feature: JWT Secret Retention Policy
|
|||||||
Then the primary secret should not be removed
|
Then the primary secret should not be removed
|
||||||
And the primary secret should remain active
|
And the primary secret should remain active
|
||||||
|
|
||||||
|
@critical @admin-introspection
|
||||||
|
Scenario: Admin metadata endpoint exposes structure without leaking secret values
|
||||||
|
Given a primary JWT secret exists
|
||||||
|
And I add a secondary JWT secret "test-secret-do-not-leak-please-12345"
|
||||||
|
When I request the JWT secrets metadata endpoint
|
||||||
|
Then the status code should be 200
|
||||||
|
And the metadata should contain 2 secrets
|
||||||
|
And the metadata should NOT contain the secret value "test-secret-do-not-leak-please-12345"
|
||||||
|
And every secret in the metadata should have a SHA-256 fingerprint
|
||||||
|
|
||||||
@todo
|
@todo
|
||||||
Scenario: Multiple secrets with different ages
|
Scenario: Multiple secrets with different ages
|
||||||
Given I have 3 JWT secrets of different ages
|
Given I have 3 JWT secrets of different ages
|
||||||
|
|||||||
@@ -822,3 +822,61 @@ func (s *JWTRetentionSteps) andSuggestRemediationSteps() error {
|
|||||||
// Verify remediation suggestions
|
// Verify remediation suggestions
|
||||||
return godog.ErrPending
|
return godog.ErrPending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Admin metadata introspection steps (PR #51 + this scenario)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// iAddASecondaryJWTSecretNamed adds a secret with a specific value via the
|
||||||
|
// admin API. Used by the admin-introspection scenario to verify that the
|
||||||
|
// metadata endpoint returns metadata only, not the secret value.
|
||||||
|
func (s *JWTRetentionSteps) iAddASecondaryJWTSecretNamed(secretValue string) error {
|
||||||
|
s.SetLastSecret(secretValue)
|
||||||
|
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
|
||||||
|
"secret": secretValue,
|
||||||
|
"is_primary": "false",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// iRequestTheJWTSecretsMetadataEndpoint hits GET /api/v1/admin/jwt/secrets.
|
||||||
|
func (s *JWTRetentionSteps) iRequestTheJWTSecretsMetadataEndpoint() error {
|
||||||
|
return s.client.Request("GET", "/api/v1/admin/jwt/secrets", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// theMetadataShouldContainNSecrets verifies the response count field.
|
||||||
|
func (s *JWTRetentionSteps) theMetadataShouldContainNSecrets(expected int) error {
|
||||||
|
body := string(s.client.GetLastBody())
|
||||||
|
expectedFragment := `"count":` + strconv.Itoa(expected)
|
||||||
|
if !strings.Contains(body, expectedFragment) {
|
||||||
|
return fmt.Errorf("expected response to contain %q, got: %s", expectedFragment, body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// theMetadataShouldNotContainTheSecretValue is the SECURITY-CRITICAL
|
||||||
|
// assertion. If the response contains the raw secret string anywhere,
|
||||||
|
// the endpoint has leaked. This is the property the metadata-only design
|
||||||
|
// is supposed to guarantee.
|
||||||
|
func (s *JWTRetentionSteps) theMetadataShouldNotContainTheSecretValue(secretValue string) error {
|
||||||
|
body := string(s.client.GetLastBody())
|
||||||
|
if strings.Contains(body, secretValue) {
|
||||||
|
return fmt.Errorf("SECURITY: response leaked the secret value %q (response body: %s)", secretValue, body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// everySecretInTheMetadataShouldHaveASHA256Fingerprint asserts the
|
||||||
|
// secret_sha256 field is present and non-empty for each entry. Cheap
|
||||||
|
// regex-style check on the JSON body.
|
||||||
|
func (s *JWTRetentionSteps) everySecretInTheMetadataShouldHaveASHA256Fingerprint() error {
|
||||||
|
body := string(s.client.GetLastBody())
|
||||||
|
// Expect at least one occurrence of "secret_sha256":"<non-empty>"
|
||||||
|
if !strings.Contains(body, `"secret_sha256":"`) {
|
||||||
|
return fmt.Errorf("response does not include any secret_sha256 fingerprint: %s", body)
|
||||||
|
}
|
||||||
|
// Reject obviously-empty values
|
||||||
|
if strings.Contains(body, `"secret_sha256":""`) {
|
||||||
|
return fmt.Errorf("at least one secret_sha256 fingerprint is empty in response: %s", body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client, s
|
|||||||
ctx.Step(`^I should receive configuration validation error$`, sc.jwtRetentionSteps.iShouldReceiveConfigurationValidationError)
|
ctx.Step(`^I should receive configuration validation error$`, sc.jwtRetentionSteps.iShouldReceiveConfigurationValidationError)
|
||||||
ctx.Step(`^the error should mention "([^"]*)"$`, sc.jwtRetentionSteps.theErrorShouldMention)
|
ctx.Step(`^the error should mention "([^"]*)"$`, sc.jwtRetentionSteps.theErrorShouldMention)
|
||||||
ctx.Step(`^I have enabled Prometheus metrics$`, sc.jwtRetentionSteps.iHaveEnabledPrometheusMetrics)
|
ctx.Step(`^I have enabled Prometheus metrics$`, sc.jwtRetentionSteps.iHaveEnabledPrometheusMetrics)
|
||||||
|
// Admin metadata introspection steps (PR #51 + admin-introspection scenario)
|
||||||
|
ctx.Step(`^I add a secondary JWT secret "([^"]*)"$`, sc.jwtRetentionSteps.iAddASecondaryJWTSecretNamed)
|
||||||
|
ctx.Step(`^I request the JWT secrets metadata endpoint$`, sc.jwtRetentionSteps.iRequestTheJWTSecretsMetadataEndpoint)
|
||||||
|
ctx.Step(`^the metadata should contain (\d+) secrets$`, sc.jwtRetentionSteps.theMetadataShouldContainNSecrets)
|
||||||
|
ctx.Step(`^the metadata should NOT contain the secret value "([^"]*)"$`, sc.jwtRetentionSteps.theMetadataShouldNotContainTheSecretValue)
|
||||||
|
ctx.Step(`^every secret in the metadata should have a SHA-256 fingerprint$`, sc.jwtRetentionSteps.everySecretInTheMetadataShouldHaveASHA256Fingerprint)
|
||||||
ctx.Step(`^I should see "([^"]*)" metric increment$`, sc.jwtRetentionSteps.iShouldSeeMetricIncrement)
|
ctx.Step(`^I should see "([^"]*)" metric increment$`, sc.jwtRetentionSteps.iShouldSeeMetricIncrement)
|
||||||
ctx.Step(`^I should see "([^"]*)" metric decrease$`, sc.jwtRetentionSteps.iShouldSeeMetricDecrease)
|
ctx.Step(`^I should see "([^"]*)" metric decrease$`, sc.jwtRetentionSteps.iShouldSeeMetricDecrease)
|
||||||
ctx.Step(`^I should see "([^"]*)" histogram update$`, sc.jwtRetentionSteps.iShouldSeeHistogramUpdate)
|
ctx.Step(`^I should see "([^"]*)" histogram update$`, sc.jwtRetentionSteps.iShouldSeeHistogramUpdate)
|
||||||
|
|||||||
Reference in New Issue
Block a user