secretrotation

package
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 8, 2024 License: MIT Imports: 4 Imported by: 1

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:

  1. In the SecretHolder, the secret rotation can happen anytime (but not too frequent).
  2. The providers can load the secrets anytime.
  3. 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

Examples

Constants

View Source
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

type Manager struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

func New

func New() *Manager

func (*Manager) Allowed

func (m *Manager) Allowed(in Secret) bool

Allowed checks if a given key match the secrets.

func (*Manager) AllowedNonConstant

func (m *Manager) 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 (*Manager) Current

func (m *Manager) Current() (Secret, error)

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

type RotatingSecret

type RotatingSecret struct {
	Previous Secret
	Current  Secret
	Pending  Secret
}

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"

type Secret

type Secret string

func (Secret) IsAllowed added in v0.5.0

func (s Secret) IsAllowed(key Secret) bool

IsAllowed will check if a given key matches the secrets

func (Secret) RedactSecret

func (s Secret) RedactSecret(in string) string

func (*Secret) Set

func (s *Secret) Set(in string) error

func (Secret) String

func (s Secret) String() string

func (Secret) Validate

func (s Secret) Validate() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL