Getting started¶
This tutorial walks you through redacting your first secret-bearing string, end
to end. You will sanitise a string that carries three different kinds of
credential, wrap an error the same way, and — most importantly — learn where
in your program redact belongs and where it does not.
Everything runs in one self-contained Go program so you can see each moving part.
Prerequisites¶
- Go 1.26 or newer.
- A new module to experiment in:
mkdir redact-tutorial && cd redact-tutorial
go mod init example.test/redact-tutorial
go get gitlab.com/phpboyscout/go/redact
Step 1 — Redact a string¶
redact.String takes any string and returns a sanitised copy. It is safe to
call on anything: it never panics, and if nothing sensitive matches it returns
the input unchanged.
Start with a string that hides three secrets at once — a credential in the URL
userinfo, an api_key= query parameter, and a bare sk- provider token:
package main
import (
"fmt"
"gitlab.com/phpboyscout/go/redact"
)
func main() {
raw := "request to https://admin:hunter2@api.example.com/v1?api_key=sk-abc123XYZ" +
" failed: leaked sk-abc123def456ghi789jkl"
fmt.Println(redact.String(raw))
}
Run it:
You will see:
Three independent rules fired, in order:
- URL userinfo —
admin:hunter2@became<redacted>@. The scheme and host are preserved so the URL is still recognisable. - Credential query parameter —
api_key=sk-abc123XYZbecameapi_key=***. The parameter name survives; its value is masked whole. - Provider prefix — the standalone
sk-abc123def456ghi789jkltoken becamesk-***. The literalsk-prefix is kept for readability; everything after it is gone.
Notice how much shape survives. Redaction is not deletion — you keep enough structure to debug with, and lose only the secret.
Step 2 — Redact an error¶
Most secrets leak through error messages: a failed request wraps the URL it
called, a config loader quotes the token it rejected. redact.Error is a
nil-safe convenience wrapper — it is exactly redact.String(err.Error()), but
returns "" when the error is nil, so you never have to guard the call.
package main
import (
"errors"
"fmt"
"gitlab.com/phpboyscout/go/redact"
)
func main() {
err := errors.New("dial https://svc:p4ssw0rd@db.internal:5432 refused")
fmt.Println(redact.Error(err)) // dial https://<redacted>@db.internal:5432 refused
fmt.Println(redact.Error(nil)) // (empty line)
}
Reach for redact.Error at the point where you hand an error to a logger, a
telemetry span, or a user-facing report — not before.
Step 3 — Know where to call it¶
This is the part that matters most, and the part a pattern list cannot decide for you.
Call redact.String / redact.Error on the way out — anywhere a
caller-supplied or environment-derived string crosses the boundary into a
surface you do not fully control:
- telemetry and tracing exporters (OTLP, vendor SDKs);
- distributed / shipped log lines that land in an aggregator;
- error reports sent to a third-party issue tracker or crash service;
- metrics labels and any managed observability ingest.
Do not reach for it on host-only debug logs that never leave the machine. Those are inside your trust boundary and may genuinely need the raw content to be useful for debugging. Redacting them buys you nothing and costs you signal.
The rule of thumb: redact at the last hop before data leaves your control, once, close to the exporter. See Redact at the boundary for how to wire that up as a single choke point.
Success criterion¶
The program in Step 1 prints the redacted line with <redacted>@, api_key=***,
and sk-***. If you see the raw secrets, check that you printed the return value
of redact.String rather than raw — String returns a new string and never
mutates its input.
Next steps¶
- Apply redaction at your telemetry/log choke point: Redact at the boundary.
- Scrub HTTP header values in logging middleware: Redact HTTP headers.
- Understand why this boundary exists: Threat model.
- See exactly what is and is not caught: The rule catalogue & its limits.