Secret Leak Detection (Scanner Operations)
Status: IMPLEMENTED (2026-01-04). Feature is production-ready.
Audience: Scanner operators, Security Guild, Docs Guild, Offline Kit maintainers.
This runbook is the operator’s guide to the Stella Ops Scanner secret-leak detection analyzer: how to deploy and verify its signed rule bundles, enable the analyzer, author secret-aware policy, and operate the configuration API, alerts, and UI.
Implementation Status
| Component | Status | Sprint (Archived) |
|---|---|---|
StellaOps.Scanner.Analyzers.Secrets plugin | IMPLEMENTED | SPRINT_20260104_002 |
| Rule bundle infrastructure | IMPLEMENTED | SPRINT_20260104_003 |
Policy DSL predicates (secret.*) | IMPLEMENTED | SPRINT_20260104_004 |
| Offline Kit integration | IMPLEMENTED | SPRINT_20260104_005 |
| Configuration API | IMPLEMENTED | SPRINT_20260104_006 |
| Alert Integration | IMPLEMENTED | SPRINT_20260104_007 |
| UI Components | IMPLEMENTED | SPRINT_20260104_008 |
| Surface.Secrets (credential delivery) | IMPLEMENTED | N/A (already complete) |
Note: All secret leak detection components are now fully implemented and production-ready. Surface.Secrets (operational credential management) remains a separate, independent feature.
1. Scope & goals
- Introduce the
StellaOps.Scanner.Analyzers.Secretsplug-in, which executes deterministic rule bundles against layer content during scans. - Ensure every finding is reproducible: rule bundles are DSSE-signed, versioned, and shipped with the Offline Kit.
- Surface policy-ready evidence (
secret.leak) so tenants can enforce block/warn flows usingstella-dsl@1predicates. - Preserve sovereignty: rule bundles install locally, no outbound telemetry, masking is enforced before data leaves the worker.
2. Prerequisites
| Requirement | Notes |
|---|---|
| Analyzer binaries | Deploy StellaOps.Scanner.Analyzers.Secrets alongside Scanner Worker (packaged with the standard container images). |
| Surface libraries | Surface.Secrets, Surface.Validation, and Surface.Env must already be configured (see the Surface Secrets design dossier). |
| Experimental flag | Enable scanner.features.experimental["secret-leak-detection"] = true on both WebService and Worker. |
| Policy readiness | Import predicates from docs/modules/policy/secret-leak-detection-readiness.md into tenant policy packs. |
| Offline Kit | Update to an Offline Kit that includes offline/rules/secrets/<release>/ before enabling production scans. |
3. Rule bundle lifecycle
Rule bundles ship in the Offline Kit under offline/rules/secrets/<release>/ (Export Center is retired; OfflineKit owns bundle packaging).
| File | Purpose | Notes |
|---|---|---|
secrets.ruleset.manifest.json | Lists rule IDs, versions, severity defaults, and hash digests. | Consume during policy drift audits. |
secrets.ruleset.rules.jsonl | Newline-delimited definitions (regex/entropy metadata, masking hints). | Loaded by the analyzer at startup. |
secrets.ruleset.dsse.json | DSSE envelope (HMAC-SHA256 signature). | Verify before distributing bundles. |
3.1 Creating custom bundles
Organizations can create custom rule bundles with additional detection patterns:
Not a CLI command at this commit. The
scanner secrets bundlegroup hasimport,verify,listandinfo; there is nocreatesubcommand. Bundles are built and signed byBundleBuilderandBundleSignerinsrc/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Bundles/. The examples below describe the intended shape only.
# Create a bundle from rule definition files
stella secrets bundle create \
--output ./bundles/custom-2026.01 \
--bundle-id custom.secrets.ruleset \
--version 2026.01 \
--rules ./custom-rules/*.json
# Create and sign in one step
stella secrets bundle create \
--output ./bundles/custom-2026.01 \
--bundle-id custom.secrets.ruleset \
--version 2026.01 \
--rules ./custom-rules/*.json \
--sign \
--key-id my-org-secrets-signer \
--shared-secret-file /path/to/signing.key
Rule definition files must follow the JSON schema:
{
"id": "myorg.secrets.internal-api-key",
"version": "1.0.0",
"name": "Internal API Key",
"description": "Detects internal API keys with MYORG prefix",
"type": "regex",
"pattern": "MYORG_[A-Z0-9]{32}",
"severity": "high",
"confidence": "high",
"keywords": ["MYORG_"],
"filePatterns": ["*.env", "*.yaml", "*.json"],
"enabled": true
}
Rule ID requirements:
- Must be namespaced (e.g.,
myorg.secrets.rule-name) - Must start with lowercase letter
- May contain lowercase letters, digits, dots, and hyphens
3.2 Verifying bundles
Verify bundle integrity and signature before deployment:
# Basic verification (checks SHA-256 integrity)
stella scanner secrets bundle verify \
--bundle ./bundles/2026.01
# Full verification with signature check
stella scanner secrets bundle verify \
--bundle ./bundles/2026.01 \
--secret-file /path/to/signing.key
# Verify with trusted key ids (repeat --trusted-key-id per key)
stella scanner secrets bundle verify \
--bundle ./bundles/2026.01 \
--secret-file /path/to/signing.key \
--trusted-key-id stellaops-secrets-signer \
--trusted-key-id my-org-signer
Verification is local: it checks the SHA-256 integrity hash and the HMAC DSSE signature and makes no network call, so the same command works in air-gapped environments. --require-signature defaults to true, so an unsigned bundle fails.
RequireRekorProof is a fail-closed requirement. A bundle that merely carries a Rekor log id is not treated as verified transparency evidence unless a real Rekor verifier is wired for the secrets bundle path. Signed SBOM archive/offline-kit local Rekor verification does not automatically make secrets rule bundles transparency-positive; until that path calls the verifier, required Rekor proof verification returns an invalid bundle result instead of a warning plus otherwise-successful verification.
The former stella excititor verify alternative targeted the retired Excititor service and is not a supported verification path; use stella scanner secrets bundle verify.
3.3 Deploying bundles
Once verified, copy the bundle to the worker:
/opt/stellaops/plugins/scanner/analyzers/secrets/
|- secrets.ruleset.manifest.json
|- secrets.ruleset.rules.jsonl
|- secrets.ruleset.dsse.json
Restart the worker so the analyzer reloads the updated bundle. Bundles are immutable; upgrading requires replacing all three files and restarting.
See secrets-bundle-rotation.md for rotation procedures and rollback instructions.
4. Enabling the analyzer
Toggle the feature flag (WebService + Worker):
scanner: features: experimental: secret-leak-detection: true(Environment alternative:
SCANNER__FEATURES__EXPERIMENTAL__secret-leak-detection=true.)Configure retention (WebService):
scanner: storage: migrations: - Scanner.Analysis.SecretFindingsTtlThe migration adds
secretFindingsrecords toScanAnalysisStorewith the standard TTL (default 90 days). Adjust PostgreSQL retention policy via the deployment overlay if longer retention is required.Activate policy ingestion (WebService):
scanner: runtime: enableSecretFindings: true(Experimental builds gate secret evidence behind this toggle to avoid surprising downstream consumers.)
Roll scanner hosts. Apply the configuration, roll WebService first, then Workers. Verify the startup logs contain
SecretsAnalyzerHostandSecretLeakDetection: Enabled.
5. Configuration API
The secret detection feature provides a REST API for per-tenant configuration:
The eight settings and exception routes below require their tenantId UUID to match the canonical selected tenant before calling the owning service. Ordinary principals select through their validated tenant claim. Global administrators retain canonical-header tenant selection; the route must match that selection too. Missing, unresolved or empty UUID selections return 400; a different route tenant returns 403. Scanner currently has no slug-to-UUID directory resolver, so these routes fail closed for an unresolved slug rather than deriving an identity. The rules-category catalog has no tenant target and keeps its existing scope requirement.
These endpoints store administrative records. Their CRUD contract does not establish that a runtime matcher, revelation path or alert dispatcher consumed the persisted settings.
5.1 Settings Endpoints
Routes as registered by MapSecretDetectionSettingsEndpoints (src/Scanner/StellaOps.Scanner.WebService/Endpoints/SecretDetectionSettingsEndpoints.cs):
GET /api/v1/secrets/config/settings/{tenantId} # scanner.secrets.settings.read
POST /api/v1/secrets/config/settings/{tenantId} # scanner.secrets.settings.write
PUT /api/v1/secrets/config/settings/{tenantId} # scanner.secrets.settings.write
GET /api/v1/secrets/config/rules/categories # scanner.secrets.settings.read
5.2 Exception Pattern Endpoints
GET /api/v1/secrets/config/exceptions/{tenantId} # scanner.secrets.exceptions.read
GET /api/v1/secrets/config/exceptions/{tenantId}/{exceptionId} # scanner.secrets.exceptions.read
POST /api/v1/secrets/config/exceptions/{tenantId} # scanner.secrets.exceptions.write
PUT /api/v1/secrets/config/exceptions/{tenantId}/{exceptionId} # scanner.secrets.exceptions.write
DELETE /api/v1/secrets/config/exceptions/{tenantId}/{exceptionId} # scanner.secrets.exceptions.write
An exception has a stable UUID and a tenant-specific unique name. Updates preserve its identity and can set or clear optional file-glob and expiry values. Expiry excludes it from active reads without deleting it. Explicit deletion remains scoped to tenant and exception UUID and cascades to its match-log rows through the existing foreign key. This is not automatic age reclamation.
5.3 Revelation Policy
The stored revelation policy describes the requested display behavior. Runtime consumption of these API-persisted settings requires separate integration evidence:
| Policy | Display | Use Case |
|---|---|---|
FullMask | [REDACTED] | Maximum security, compliance reports |
PartialReveal | AKIA****WXYZ | Default for UI, allows identification |
FullReveal | Full value | Incident response (requires elevated permissions) |
5.4 Alert Configuration
The stored alert configuration describes the requested notification behavior; persistence alone does not qualify delivery through Notify:
- Destinations: Slack, Teams, Email, Webhook, PagerDuty
- Rate Limiting: Max alerts per scan (default: 10)
- Deduplication: 24-hour window to prevent duplicate alerts
- Severity Routing: Route critical findings to different channels
6. Policy patterns
The analyzer emits secret.leak evidence with the shape:
{
"ruleId": "stellaops.secrets.aws-access-key",
"ruleVersion": "2026.01.0",
"severity": "high",
"confidence": "high",
"file": "/app/config.yml",
"line": 42,
"mask": "AKIA********B7",
"bundleId": "secrets.ruleset",
"bundleVersion": "2026.01"
}
Policy DSL helpers introduced with this release:
| Helper | Description |
|---|---|
secret.hasFinding(ruleId?, severity?, confidence?) | Returns true if any finding matches the filter. |
secret.bundle.version(requiredVersion) | Ensures the active bundle meets or exceeds a version. |
secret.match.count(ruleId?) | Returns the number of findings (useful for thresholds). |
secret.mask.applied | Returns true if masking was successfully applied. |
secret.path.allowlist(patterns) | Returns true if all findings are in allowed paths. |
Sample policy (policies/secret-blocker.stella):
policy "Secret Leak Guard" syntax "stella-dsl@1" {
metadata {
description = "Block high-confidence secret leaks"
tags = ["secrets","compliance"]
}
rule block_high_confidence priority 10 {
when secret.hasFinding(severity: "high", confidence: "high")
then escalate to "block";
because "High severity secret leak detected";
}
rule require_current_bundle priority 5 {
when not secret.bundle.version("2026.01")
then warn message "Secret leak bundle out of date";
}
}
Tenants that prefer staged rollout can downgrade low-confidence findings:
rule low_confidence_warn priority 20 {
when secret.hasFinding(confidence: "low")
then annotate decision.notes := "Investigate masked payload";
else ignore;
}
7. UI Components
The secret detection UI is available at /tenants/{tenantId}/secrets/:
7.1 Settings Page
- General Tab: Enable/disable detection, revelation policy, rule categories
- Exceptions Tab: Manage allowlist patterns for false positive suppression
- Alerts Tab: Configure alert destinations and thresholds
7.2 Findings List
- Filterable by severity, status, rule category
- Masked value display with conditional reveal
- Pagination and export support
7.3 Exception Manager
- Create/edit/delete exception patterns
- Regex validation with test mode
- Expiration dates for temporary exceptions
8. Observability & reporting
- Metrics:
scanner.secret.finding_total{tenant,ruleId,severity,confidence}increments per finding. Add Prometheus alerts for spikes. - Logs:
SecretsAnalyzerHostlogs bundle version on load and emits warnings when masking fails (payload never leaves memory). - Traces: Each analyzer run adds a
scanner.secrets.scanspan with rule counts and wall-clock timing. - Reports / CLI: Scan reports include a
secretFindingsarray; CLI diff/export surfaces render masked snippets plus remediation guidance.
9. Troubleshooting
| Symptom | Resolution |
|---|---|
| Analyzer disabled at startup | Confirm feature flag and bundle files exist; check plugins/scanner/analyzers/secrets permissions (640). |
| No findings despite seeded secrets | Ensure bundle hash matches manifest. Run worker with --secrets-trace (debug build) to log matched rules locally. |
| Policy marks findings as unknown | Upgrade tenant policies to include secret.* helpers; older policies silently drop the namespace. |
| Air-gapped verification fails | Verification makes no network call; check the bundle directory and the key file, then rerun stella scanner secrets bundle verify --bundle <dir> --secret-file <key>. |
| Signature verification failed | Check shared secret matches signing key. Verify DSSE envelope exists and is not corrupted. See signature troubleshooting below. |
| Bundle integrity check failed | Rules file was modified after signing. Re-download bundle or rebuild from sources. |
| Key not in trusted list | Add the signer key id with --trusted-key-id or update scanner.secrets.trustedKeyIds configuration. |
9.1 Signature verification troubleshooting
“Signature verification failed” error:
Verify the shared secret is correct:
# Check secret file exists and is readable cat /path/to/signing.key | wc -c # Should output the key length (typically 32-64 bytes for base64-encoded keys)Check the DSSE envelope format:
# Verify envelope is valid JSON jq . ./bundles/2026.01/secrets.ruleset.dsse.jsonConfirm manifest matches envelope payload:
# The envelope payload (base64url-decoded) should match the manifest content # excluding the signatures field
“Rules file integrity check failed” error:
Recompute the SHA-256 hash:
sha256sum ./bundles/2026.01/secrets.ruleset.rules.jsonlCompare with manifest:
jq -r '.integrity.rulesSha256' ./bundles/2026.01/secrets.ruleset.manifest.jsonIf hashes differ, the rules file was modified. Re-download or rebuild the bundle.
“Bundle is not signed” error:
The bundle was created without the --sign flag. Either:
- Rebuild with signing:
stella secrets bundle create ... --sign --key-id <key> - Skip signature verification:
--skip-signature-verification(not recommended for production)
10. References
- Policy: secret-leak detection readiness
- Benchmarks: secrets deep dive
- Surface Secrets design dossier
- Architecture overview — runtime inventory (Scanner)
- Secrets bundle rotation
