package kubernetes import ( "context" "fmt" "os" "github.com/hashicorp/vault/api" ) type KubernetesAuth struct { roleName string mountPath string serviceAccountToken string } var _ api.AuthMethod = (*KubernetesAuth)(nil) type LoginOption func(a *KubernetesAuth) error const ( defaultMountPath = "kubernetes" defaultServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" ) // NewKubernetesAuth creates a KubernetesAuth struct which can be passed to // the client.Auth().Login method to authenticate to Vault. The roleName // parameter should be the name of the role in Vault that was created with // this app's Kubernetes service account bound to it. // // The Kubernetes service account token JWT is retrieved from // /var/run/secrets/kubernetes.io/serviceaccount/token by default. To change this // path, pass the WithServiceAccountTokenPath option. To instead pass the // JWT directly as a string, or to read the value from an environment // variable, use WithServiceAccountToken and WithServiceAccountTokenEnv respectively. // // Supported options: WithMountPath, WithServiceAccountTokenPath, WithServiceAccountTokenEnv, WithServiceAccountToken func NewKubernetesAuth(roleName string, opts ...LoginOption) (*KubernetesAuth, error) { if roleName == "" { return nil, fmt.Errorf("no role name was provided") } a := &KubernetesAuth{ roleName: roleName, mountPath: defaultMountPath, } // Loop through each option for _, opt := range opts { // Call the option giving the instantiated // *KubernetesAuth as the argument err := opt(a) if err != nil { return nil, fmt.Errorf("error with login option: %w", err) } } if a.serviceAccountToken == "" { token, err := readTokenFromFile(defaultServiceAccountTokenPath) if err != nil { return nil, fmt.Errorf("error reading service account token from default location: %w", err) } a.serviceAccountToken = token } // return the modified auth struct instance return a, nil } func (a *KubernetesAuth) Login(ctx context.Context, client *api.Client) (*api.Secret, error) { if ctx == nil { ctx = context.Background() } loginData := map[string]interface{}{ "jwt": a.serviceAccountToken, "role": a.roleName, } path := fmt.Sprintf("auth/%s/login", a.mountPath) resp, err := client.Logical().WriteWithContext(ctx, path, loginData) if err != nil { return nil, fmt.Errorf("unable to log in with Kubernetes auth: %w", err) } return resp, nil } func WithMountPath(mountPath string) LoginOption { return func(a *KubernetesAuth) error { a.mountPath = mountPath return nil } } // WithServiceAccountTokenPath allows you to specify a different path to // where your application's Kubernetes service account token is mounted, // instead of the default of /var/run/secrets/kubernetes.io/serviceaccount/token func WithServiceAccountTokenPath(pathToToken string) LoginOption { return func(a *KubernetesAuth) error { token, err := readTokenFromFile(pathToToken) if err != nil { return fmt.Errorf("unable to read service account token from file: %w", err) } a.serviceAccountToken = token return nil } } func WithServiceAccountToken(jwt string) LoginOption { return func(a *KubernetesAuth) error { a.serviceAccountToken = jwt return nil } } func WithServiceAccountTokenEnv(envVar string) LoginOption { return func(a *KubernetesAuth) error { token := os.Getenv(envVar) if token == "" { return fmt.Errorf("service account token was specified with an environment variable with an empty value") } a.serviceAccountToken = token return nil } } func readTokenFromFile(filepath string) (string, error) { jwt, err := os.ReadFile(filepath) if err != nil { return "", fmt.Errorf("unable to read file containing service account token: %w", err) } return string(jwt), nil }