Documentation ¶
Overview ¶
Package secretrotation helps with secret rotation when a service must accept old and new secrets for a time.
- SecretHolder is a DB service that provides the secret. For example a AWS Secret Manager service. This should provide 3 secrets (default: comma separated).
- Provider is for example a web server validating secret in incoming API calls. At any time, it always accepts any of the 3 secrets.
- Consumer is for example an application calling the web server. It is always sending the secret in the middle.
The reason for the 3 secrets is we always want a valid secret, given:
- In the SecretHolder, the secret rotation can happen anytime (but not too frequent).
- The providers can load the secrets anytime.
- The consumers can load the secrets anytime.
Assertions:
- The refresh rate on the Providers and Consumers is faster than the secret rotation frequency in the secret holder.
- We have 3 secrets: {PREVIOUS,CURRENT,PENDING}. This is AWS design and that seems robust.
- Deserializing a single value string (without coma) will return value=PREVIOUS=CURRENT=PENDING.
- Secrets should never contain coma (as this is the delimiter for serialization)
See: https://docs.aws.amazon.com/secretsmanager/latest/userguide/getting-started.html
Alternative similar design:
- Store only 2 secrets in the SecretHolder but with the change time, and share rotation duration details with the consumer. (A lot more complicated)
Example ¶
package main import ( "errors" "fmt" "github.com/vincentkerdraon/configo/secretrotation" ) func main() { m := secretrotation.New() //Init not done _, err := m.Current() if !errors.Is(err, secretrotation.MissingInitValuesError{}) { panic(err) } //Provide input rs := secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "my_secretC") err = m.Set(rs) handleErr(err) //Check received secret is allowed (this is the producer point of view) if !m.Allowed("my_secretC") { panic("expected secret OK") } //Get secret to use (this is the consumer point of view) secret, err := m.Current() handleErr(err) fmt.Print(secret) } func handleErr(err error) { if err != nil { panic(err) } }
Output: my_secretB
Index ¶
- Constants
- type InvalidSecretError
- type Manager
- type MissingInitValuesError
- type RotatingSecret
- func (rs RotatingSecret) Allowed(in Secret) bool
- func (rs RotatingSecret) AllowedNonConstant(in Secret) bool
- func (rs *RotatingSecret) Deserialize(s string) error
- func (rs RotatingSecret) Range(f func(Secret) (continueRange bool))
- func (rs RotatingSecret) RedactSecret(in string) string
- func (rs RotatingSecret) Serialize() string
- func (rs *RotatingSecret) Set(s string) error
- func (rs RotatingSecret) Validate() error
- type Secret
Examples ¶
Constants ¶
const SecretRedacted = "[redacted]"
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type InvalidSecretError ¶ added in v0.2.0
type InvalidSecretError struct {
Err error
}
func (InvalidSecretError) Error ¶ added in v0.2.0
func (err InvalidSecretError) Error() string
func (InvalidSecretError) Unwrap ¶ added in v0.2.0
func (err InvalidSecretError) Unwrap() error
type Manager ¶
func (*Manager) AllowedNonConstant ¶
AllowedNonConstant checks if a given key match the secrets. This is NOT using the crypto security on timing attacks. This is faster than Allowed()
func (*Manager) Current ¶
Current returns the secret to use when the consumer is calling the provider.
func (*Manager) RotatingSecret ¶
func (m *Manager) RotatingSecret() (RotatingSecret, error)
func (*Manager) Set ¶
func (m *Manager) Set(rs RotatingSecret) error
type MissingInitValuesError ¶ added in v0.2.0
type MissingInitValuesError struct{}
func (MissingInitValuesError) Error ¶ added in v0.2.0
func (MissingInitValuesError) Error() string
type RotatingSecret ¶
func NewRotatingSecret ¶
func NewRotatingSecret(previous, current, pending Secret) RotatingSecret
func (RotatingSecret) Allowed ¶ added in v0.5.1
func (rs RotatingSecret) Allowed(in Secret) bool
Allowed checks if a given key match the secrets.
Example ¶
package main import ( "github.com/vincentkerdraon/configo/secretrotation" ) func main() { rs := secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "my_secretC") // For example we receive a Shared secret in a http request, and we want to check it matches one of the known secrets // This function will always take the same time, in order to make it harder to detect how close an attack is to the solution by looking at the processing time. if !rs.Allowed("my_secretC") { panic("expect ok") } }
Output:
func (RotatingSecret) AllowedNonConstant ¶ added in v0.5.1
func (rs RotatingSecret) AllowedNonConstant(in Secret) bool
AllowedNonConstant checks if a given key match the secrets. This is NOT using the crypto security on timing attacks. This is faster than Allowed()
func (*RotatingSecret) Deserialize ¶
func (rs *RotatingSecret) Deserialize(s string) error
Deserialize will populate the RotatingSecret object based on the string value
If string empty => error. If 1 part string => all 3 values of the secret will be the same. If 3 part string, comma separated => set into RotatingSecret. Else => error
func (RotatingSecret) Range ¶
func (rs RotatingSecret) Range(f func(Secret) (continueRange bool))
Range iterates over the secrets
func (RotatingSecret) RedactSecret ¶
func (rs RotatingSecret) RedactSecret(in string) string
Example ¶
package main import ( "fmt" "github.com/vincentkerdraon/configo/secretrotation" ) func main() { rs := secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "my_secretC") const input = "my string including my_secretA" const expected = "my string including [redacted]" redacted := rs.RedactSecret(input) if redacted != expected { panic("expect ok") } fmt.Printf("redacted=%q\n", redacted) }
Output: redacted="my string including [redacted]"
func (RotatingSecret) Serialize ¶
func (rs RotatingSecret) Serialize() string
Example ¶
package main import ( "fmt" "github.com/vincentkerdraon/configo/secretrotation" ) func main() { rs := secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "my_secretC") serial := rs.Serialize() //Serialize rs2 := secretrotation.RotatingSecret{} if err := rs2.Deserialize(serial); err != nil { panic("expect ok") } if rs2.Current != rs.Current { panic("expect ok") } fmt.Printf("serial=%q, rs2=%+v\n", serial, rs2) }
Output: serial="my_secretA,my_secretB,my_secretC", rs2={Previous:my_secretA Current:my_secretB Pending:my_secretC}
func (*RotatingSecret) Set ¶ added in v0.3.4
func (rs *RotatingSecret) Set(s string) error
func (RotatingSecret) Validate ¶
func (rs RotatingSecret) Validate() error
Example ¶
package main import ( "fmt" "github.com/vincentkerdraon/configo/secretrotation" ) func main() { rs := secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "my_secretC") if rs.Validate() != nil { panic("expect ok") } rs = secretrotation.NewRotatingSecret("my_secretA", "my_secretB", "") if rs.Validate() == nil { panic("expect fail") } fmt.Printf("Validate=%q\n", rs.Validate()) }
Output: Validate="empty secret"