open-vault/api/auth/kubernetes/kubernetes.go
2022-04-06 11:20:34 -04:00

134 lines
3.8 KiB
Go

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
}