Logfusc: Surefire Secret Redaction for Logs and Traces

Stop scrubbing logs. Make your secrets unloggable.

A gopher dressed as a private eye, investigating documents.

How many countless hours have you wasted trying to find the cause of bugs in code without proper instrumentation? Bugs that would be trivial if only someone had logged the inputs, outputs and errors of the right function at the right time?

I'm over it. I'd rather build things than spend my day chasing heisenbugs.

That's why I log everything. Every request that comes in, every call to my business logic, every database request and every error or interesting event that happens along the way. I log it all – including and especially function arguments.

If bad inputs have slipped through the validation net, I want to know about it now.

Here's the problem: running applications are full of secrets. API keys, database credentials, pre-hash user passwords. They're in there, waiting to leak into nefarious hands.

Speaking of hands – hands up if you've ever accidentally logged a secret? I have. It's a pain in the ass. You have to fix the offending code and scrub the record clean.

This makes us avoid logging the fat, information-rich structs that get passed around our program. Useful as that might be for debugging, it's a security risk just waiting to spoil your day.

And who has time to manually implement fmt.Stringer for every argument they might want to log? Not me.

That's why I've released logfusc, a Go package that gives you bomb-proof secret redaction for logs and traces.

Stop scrubbing logs in the aftermath of preventable human errors. Stop hand-coding string representations of must-log structs just to keep their privates private. Make secrets unloggable.

logfusc.Secret

logfusc.Secret is a generic wrapper for any type that you want to redact from logs, traces and other outputs.

Secret implements fmt.Stringer and fmt.GoStringer, so no matter how hard you try to format it, it doesn't give up its secret.

password := "do not log!"
secret := logfusc.NewSecret(password)

fmt.Printf("%s\n", secret)
// => logfusc.Secret[string]{REDACTED}

fmt.Printf("%q\n", secret)
// => "logfusc.Secret[string]{REDACTED}"

fmt.Printf("%v\n", secret)
// => "logfusc.Secret[string]{REDACTED}"

fmt.Printf("%+v\n", secret)
// => "logfusc.Secret[string]{REDACTED}"

fmt.Printf("%#v\n", secret)
// => "logfusc.Secret[string]{REDACTED}"

fmt.Printf("%x\n", secret)
// => 6c6f67667573632e5365637265745b737472696e675d7b52454441435445447d == logfusc.Secret[string]{REDACTED}

Log anything, anywhere

logfusc.Secret redacts your secrets when marshaled to a variety of formats, so you can pass complete structs to your logger without worrying about leaking sensitive data. No more manual redaction. No configuration. No leaks.

type Universe struct {
    SecretOfLife logfusc.Secret[int] `json:"secret_of_life"`
}

func main() {
  universe := Universe{
      SecretOfLife: logfusc.NewSecret(42),
  }

  b, _ := json.Marshal(universe)

  log.Println(string(b))
}

// => {"name":"alice","secret_of_life":"logfusc.Secret[int]{REDACTED}"}

So far, Secret satisfies:

Decode directly to logfusc.Secret

Protect secrets at the boundaries of your service by decoding them directly into logfusc.Secret.

type RegisterRequest struct {
  Email string `json:"email"`
  Password logfusc.Secret[string] `json:"password"`
}

func Register(w http.ResponseWriter, r *http.Request) {
  var req RegisterRequest
  _ = json.NewDecoder(r.Body).Decode(&req)
  fmt.Println(req.Password)
  fmt.Println(req.Password.Expose())
}

// => logfusc.Secret[string]{REDACTED}
//    password

So far, Secret satisfies:

  • json.Unmarshaler
  • More coming soon! Feel free to contribute to speed things up.

Use secrets with intention

Secret encourages you to log often and trace everything in the knowledge that your secrets are safe. That doesn't stop you working with sensitive data, but you have to do it deliberately.

secret := logfusc.NewSecret("PEM string")
log.Println(secret.Expose())

// => PEM string

Compatibility

logfusc.Secret should work as described with all sane logging libraries.

"Should" because, in Go, you can technically access the private fields of a struct using a combination of the reflect and unsafe packages. If your logger is doing this, you need a new one.

For your peace of mind, logfusc.Secret has been explicitly tested for compatibility with the following loggers:

  • log
  • logrus
  • zap
  • zerolog
  • More compatibility tests coming soon! (And yes, you can indeed contribute!)

Try logfusc for yourself!

Go forth and log freely! You can find logfusc on GitHub now.