package secrets import ( "context" "crypto/rsa" "crypto/x509" "encoding/base64" "fmt" "sync" "time" "code.crute.us/mcrute/golib/log" "code.crute.us/mcrute/golib/service" ) type Handle interface { Reference() string } type Renewal struct { Name string Critical bool Time time.Time Error error } type Credential struct { Username string `json:"username" mapstructure:"username" yaml:"username"` Password string `json:"password" mapstructure:"password" yaml:"password"` } type ApiKey struct { Key string `json:"key" mapstructure:"key" yaml:"key"` } type AWSCredential struct { AccessKeyId string `json:"access_key" mapstructure:"access_key" yaml:"access_key"` SecretAccessKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"` SessionToken string `json:"security_token" mapstructure:"security_token" yaml:"security_token"` } type RSAKey struct { Key string `json:"key" mapstructure:"key" yaml:"key"` } func (k *RSAKey) RSAPrivateKey() (*rsa.PrivateKey, error) { der, err := base64.StdEncoding.DecodeString(k.Key) if err != nil { return nil, err } pr, err := x509.ParsePKCS8PrivateKey(der) if err != nil { return nil, err } pk, ok := pr.(*rsa.PrivateKey) if !ok { return nil, fmt.Errorf("RSAKey: parsed key is not an rsa.PrivateKey") } return pk, nil } // Client is the interface that users of secrets returned by a secret // back-end should expect. This interface contains only secret related // functionality and none of the functions for running the back-end // itself. This is separate from the manager functions to make it easier // to inject stubs to code that doesn't care about the fact that a // manager may exist. type Client interface { DatabaseCredential(ctx context.Context, suffix string) (*Credential, Handle, error) Secret(ctx context.Context, suffix string, out any) (Handle, error) RawSecret(ctx context.Context, path string, out any) (Handle, error) AWSIAMUser(ctx context.Context, name string) (*AWSCredential, Handle, error) AWSAssumeRoleSimple(ctx context.Context, name string) (*AWSCredential, Handle, error) AWSAssumeRole(ctx context.Context, name string, sessionName string, ttl time.Duration) (*AWSCredential, Handle, error) WriteSecret(ctx context.Context, suffix string, out any) error Encrypt(ctx context.Context, suffix string, data []byte) (string, error) Decrypt(ctx context.Context, suffix, data string) ([]byte, error) Destroy(Handle) error MakeNonCritical(Handle) error } // ClientManager is like a Client, and contains a Client, but also // contains other runtime functionality for running the secret back-end // infrastructure that most consumers of secretes don't care about but // the main process runner does. type ClientManager interface { Client Authenticate(context.Context) error Notifications() <-chan Renewal Run(context.Context, *sync.WaitGroup) error } // MakeRenewalLogger subscribes to a ClientManager notification channel // and logs those to the logger. If a critical credential fails the // terminator callback will be called which should shut down the // application in an orderly fashion. func MakeRenewalLogger(cm ClientManager, log log.LeveledLogger, terminator func()) service.RunnerFunc { return func(ctx context.Context, wg *sync.WaitGroup) error { wg.Add(1) defer wg.Done() for { select { case r := <-cm.Notifications(): if r.Error != nil { if r.Critical { log.Errorf("Failed to renew critical secret %s due to %s", r.Name, r.Error) terminator() } else { log.Errorf("Failed to renew non-critical secret %s", r.Name) } } else { log.Infof("Renewing credential %s", r.Name) } case <-ctx.Done(): log.Infof("Shutting down secret renewal logger") return nil } } } }