Prevent Logging Secrets in Go by Using Custom Types

A quick post on how we make it harder to log secrets in Go.

 min read

In our codebase for Granted Approvals, we’ve got a structure that looks like this:

type Provider struct {	
    OrgUrl string	
    APIToken string
}

In this case, we’ve got a value named apiToken. It’s a secret, and we’d rather it doesn’t get leaked somewhere. Your secret value could also be an API token, or maybe it’s a password or a user address or anything else that you don’t want to get a Privacy Act lawsuit for.

The trouble with storing your secrets in strings like this is that it’s very easy to accidentally log them. It only takes one fmt or zap line for your secret value to be printed in plaintext and then possibly even sent to your external logging solutions. Using unexported fields can help, but Zap will still include them anyway.

If you’re reading this, then you probably already know why having sensitive data in your logs is dangerous. If you don’t, you can check out this. Or this. Or this. Or…. you get the point.

And if you’re rolling your eyes and saying I’d never log sensitive data, I’d ask: are you sure? You’re never going to accidentally type the wrong variable name in your print function? You’re never going to have a new dev join your team? Nobody on your team is ever going to be one night away from a deadline debugging why the hell the password reset function isn’t working and just go screw it, I don’t get paid enough to care about security anyway? I’m not so sure.

Our solution: custom types.

type Provider struct {	
    OrgUrl StringValue	
    APIToken SecretStringValue
}

Now instead of orgUrl and apiToken being type string, they’re now StringValue and SecretStringValue. Both of these custom types provide a small abstraction over a standard string type:

type StringValue struct {
    Value string
}

type SecretStringValue struct {
    Value string
}

The part that’s important to us though is how they both implement the Stringer and MarshalJSON interfaces:

func (s StringValue) String() string {
    return s.Value
}

func (s StringValue) MarshalJSON() ([]byte, error) {
    return json.Marshal(s.String())
}

func (s SecretStringValue) String() string {	
    return "*****"
}

func (s SecretStringValue) MarshalJSON() ([]byte, error) {	
    return json.Marshal(s.String())
}

The String() implementation for SecretStringValue will only return the redacted string “*****”, and same goes for the JSON. This makes it a whole lot more difficult to accidentally log the secret by just plugging it into fmt or zap.

So now, if you’re got something like:

oops := Provider{OrgURL:"commonfate.io",APIToken:"secret"}
fmt.Printf("%s", oops.APIToken)

or:

oops := Provider{OrgURL: "commonfate.io", APIToken: "secret"}
b, _ := json.Marshal(oops)
fmt.Print(string(b))

All you should hopefully get in return is ***** or {"OrgUrl":"commonfate.io","APIToken":"*****"}.

Is it totally foolproof? No. You can still log the secret if someone decides to deliberately print the Value, but at least now the team has clearer context on whether or not something is intended to be a Secret or just a regular Value. It makes it just that little bit harder to make a mistake — and in security, that’s all we can really ask.

If you want to check out the code for yourself, here’s a Go Playground link. And if you’re interested in seeing how we’ve implemented this in our own project, you can check out our gconfig package.

p.s.: you can totally just use string aliases instead of custom types too. we found that custom types work better for our implementation, but here's an example with string aliases too.