Redact โ Credential Stripping at Boundaries¶
pkg/redact is the shared redactor for any surface in GTB โ and in tools built on GTB โ that writes free-form strings outside the trust boundary of the local process. It is applied automatically by the telemetry collector for TrackCommandExtended error and args fields, and by HTTP middleware for known-sensitive request headers. Callers that write their own logs or telemetry events should route untrusted strings through it too.
Local-process logs that never leave the host do not need to go through this package โ those may need raw content for debugging.
Threat Model¶
Credentials reach observability surfaces by accident more often than by design:
| Vector | Example |
|---|---|
| HTTP client error wrapping a URL with a token | failed GET https://u:[email protected]/?apikey=sk-abc...: 401 |
Command invoked with --api-token=<secret> |
os.Args = ["--api-token=sk-proj-abc..."] |
OTel exporter configured with Authorization header |
middleware that logs headers dumps the token |
| Provider error message quoting back a bearer token | Authorization: Bearer abc123... appears in errMsg |
Once that content reaches a third-party ingest it is outside the operator's control โ replicated, indexed, and retained longer than intended. Redacting at the boundary is the last controllable step.
Invariants¶
- Idempotent โ
String(String(s)) == String(s). Middlewares that might double-redact cannot corrupt output. - Never panics โ any input, including zero-length or control-byte strings, is safe to pass.
- Bounded growth โ replacements are fixed-length (
***,<redacted>,<redacted-token>). Output never grows the input unboundedly. - Pure function โ same input โ same output; no side effects; safe for concurrent use.
- Original string never retained โ the redactor does not log, cache, or transmit the input anywhere.
Both invariants 1 and 2 are enforced by FuzzRedactString in CI.
API¶
import "gitlab.com/phpboyscout/go-tool-base/pkg/redact"
// Clean a free-form string before shipping.
clean := redact.String(userInput)
// Convenience wrapper over err.Error() โ returns "" for nil.
cleanErr := redact.Error(err)
// Decide whether a header name looks like it carries a credential.
if redact.IsSensitiveHeaderKey(name) {
// warn the operator, redact the value, etc.
}
// The default redaction list used by HTTP middleware and telemetry.
for _, k := range redact.SensitiveHeaderKeys { /* ... */ }
What Gets Redacted¶
| Shape | Match | Replacement |
|---|---|---|
| URL userinfo | https?://user:pass@host/... |
https://<redacted>@host/... |
| Query cred params | ?apikey=, ?api_key=, ?token=, ?secret=, ?password=, ?auth=, ?authorization=, ?signature=, ?access_token=, ?refresh_token=, ?key= |
value replaced with *** |
| Authorization header in free text | Authorization: <scheme> <token> where scheme is Bearer/Basic/Digest/ApiKey |
Authorization: <scheme> *** |
| OpenAI-family prefix | sk-[A-Za-z0-9_-]{16,} |
sk-*** |
| GitHub tokens | ghp_, gho_, ghs_, github_pat_ |
prefix + *** |
| Slack tokens | xoxb-, xoxp-, xoxa-, etc. |
prefix + *** |
| Google API key | AIza[A-Za-z0-9_-]{30,} |
AIza*** (or more, up to the first - / _) |
| AWS access key ID | AKIA[A-Z0-9]{16} |
AKIA*** |
| Fuzzy long token | any โฅ 41-char alphanumeric/_/- run not already caught |
<redacted-token> |
Known Limitations¶
- ASCII-only patterns. Virtually all provider credentials are ASCII; UTF-8 edge cases are out of scope.
- False negatives on unusual credential formats. A credential that doesn't match any catalogued pattern and is under 41 characters will slip through. Add a pattern PR if a vendor you care about has a unique shape.
- Fuzzy pattern threshold at 41 characters. Chosen to avoid false positives on UUIDs (32/36), MD5 (32), and git SHAs (40). SHA-256 hashes (64) will match โ acceptable; hashes rarely appear in error strings and over-redaction is safer than under-redaction.
- Pattern order matters. Earlier-matching patterns claim spans first. A string like
?token=+ long-opaque-value is redacted astoken=***(by the query-param rule), nottoken=<redacted-token>(the fuzzy fallback). Both outcomes are redactions; the difference is cosmetic.
Call-Site Discipline¶
pkg/redact is the entry point for untrusted-string redaction. When adding a new code path that writes to telemetry, distributed logs, or a third-party observability surface:
- Route any free-form string that may contain user- or environment-derived content through
redact.Stringorredact.Error. - Use
redact.IsSensitiveHeaderKeywhen deciding whether to log a header value or emit an operator advisory. - When in doubt, redact โ the cost is negligible (a few regex passes on a bounded string); the cost of missing is a credential in a vendor log.
Failing to route through the helper reintroduces the leakage class this package exists to close.