Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add projected service account support #288

Merged
merged 17 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 109 additions & 34 deletions agent-inject/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const (
DefaultAgentUseLeaderElector = false
DefaultAgentInjectToken = false
DefaultTemplateConfigExitOnRetryFailure = true
DefaultServiceAccountMount = "/var/run/secrets/vault.hashicorp.com/serviceaccount"
)

// Agent is the top level structure holding all the
Expand Down Expand Up @@ -95,14 +96,10 @@ type Agent struct {
//found, and the unique name of the secret which will be used for the filename.
Secrets []*Secret

// ServiceAccountName is the Kubernetes service account name for the pod.
// This is used when we mount the service account to the Vault Agent container(s).
ServiceAccountName string

// ServiceAccountPath is the path on disk where the service account JWT
// can be located. This is used when we mount the service account to the
// Vault Agent container(s).
ServiceAccountPath string
// ServiceAccountTokenVolume holds details of a volume mount for a
// Kubernetes service account token for the pod. This is used when we mount
// the service account to the Vault Agent container(s).
ServiceAccountTokenVolume *ServiceAccountTokenVolume

// Status is the current injection status. The only status considered is "injected",
// which prevents further mutations. A user can patch this annotation to force a new
Expand Down Expand Up @@ -159,6 +156,17 @@ type Agent struct {
InjectToken bool
}

type ServiceAccountTokenVolume struct {
// Name of the volume
Name string

// MountPath of the volume within vault agent containers
MountPath string

// TokenPath to the JWT token within the volume
TokenPath string
}

type Secret struct {
// Name of the secret used to identify other annotation directives, and used
// as the filename for the rendered secret file (unless FilePathAndName is
Expand Down Expand Up @@ -281,31 +289,33 @@ type VaultAgentTemplateConfig struct {

// New creates a new instance of Agent by parsing all the Kubernetes annotations.
func New(pod *corev1.Pod, patches []*jsonpatch.JsonPatchOperation) (*Agent, error) {
saName, saPath := serviceaccount(pod)
sa, err := serviceaccount(pod)
if err != nil {
return nil, err
}
var iamName, iamPath string
if pod.Annotations[AnnotationVaultAuthType] == "aws" {
iamName, iamPath = getAwsIamTokenVolume(pod)
}

agent := &Agent{
Annotations: pod.Annotations,
ConfigMapName: pod.Annotations[AnnotationAgentConfigMap],
ImageName: pod.Annotations[AnnotationAgentImage],
DefaultTemplate: pod.Annotations[AnnotationAgentInjectDefaultTemplate],
LimitsCPU: pod.Annotations[AnnotationAgentLimitsCPU],
LimitsMem: pod.Annotations[AnnotationAgentLimitsMem],
Namespace: pod.Annotations[AnnotationAgentRequestNamespace],
Patches: patches,
Pod: pod,
RequestsCPU: pod.Annotations[AnnotationAgentRequestsCPU],
RequestsMem: pod.Annotations[AnnotationAgentRequestsMem],
ServiceAccountName: saName,
ServiceAccountPath: saPath,
Status: pod.Annotations[AnnotationAgentStatus],
ExtraSecret: pod.Annotations[AnnotationAgentExtraSecret],
CopyVolumeMounts: pod.Annotations[AnnotationAgentCopyVolumeMounts],
AwsIamTokenAccountName: iamName,
AwsIamTokenAccountPath: iamPath,
Annotations: pod.Annotations,
ConfigMapName: pod.Annotations[AnnotationAgentConfigMap],
ImageName: pod.Annotations[AnnotationAgentImage],
DefaultTemplate: pod.Annotations[AnnotationAgentInjectDefaultTemplate],
LimitsCPU: pod.Annotations[AnnotationAgentLimitsCPU],
LimitsMem: pod.Annotations[AnnotationAgentLimitsMem],
Namespace: pod.Annotations[AnnotationAgentRequestNamespace],
Patches: patches,
Pod: pod,
RequestsCPU: pod.Annotations[AnnotationAgentRequestsCPU],
RequestsMem: pod.Annotations[AnnotationAgentRequestsMem],
ServiceAccountTokenVolume: sa,
Status: pod.Annotations[AnnotationAgentStatus],
ExtraSecret: pod.Annotations[AnnotationAgentExtraSecret],
CopyVolumeMounts: pod.Annotations[AnnotationAgentCopyVolumeMounts],
AwsIamTokenAccountName: iamName,
AwsIamTokenAccountPath: iamPath,
Vault: Vault{
Address: pod.Annotations[AnnotationVaultService],
ProxyAddress: pod.Annotations[AnnotationProxyAddress],
Expand All @@ -326,7 +336,6 @@ func New(pod *corev1.Pod, patches []*jsonpatch.JsonPatchOperation) (*Agent, erro
},
}

var err error
agent.Secrets = agent.secrets()
agent.Vault.AuthConfig = agent.authConfig()
agent.Inject, err = agent.inject()
Expand Down Expand Up @@ -600,8 +609,11 @@ func (a *Agent) Validate() error {
return errors.New("namespace missing from request")
}

if a.ServiceAccountName == "" || a.ServiceAccountPath == "" {
return errors.New("no service account name or path found")
if a.ServiceAccountTokenVolume == nil ||
a.ServiceAccountTokenVolume.Name == "" ||
a.ServiceAccountTokenVolume.MountPath == "" ||
a.ServiceAccountTokenVolume.TokenPath == "" {
return errors.New("no service account token volume name, mount path or token path found")
}

if a.ImageName == "" {
Expand Down Expand Up @@ -629,16 +641,79 @@ func (a *Agent) Validate() error {
return nil
}

func serviceaccount(pod *corev1.Pod) (string, string) {
var serviceAccountName, serviceAccountPath string
func serviceaccount(pod *corev1.Pod) (*ServiceAccountTokenVolume, error) {
if volumeName := pod.ObjectMeta.Annotations[AnnotationAgentServiceAccountTokenVolumeName]; volumeName != "" {
// Attempt to find existing mount point of named volume and copy mount path
for _, container := range pod.Spec.Containers {
for _, volumeMount := range container.VolumeMounts {
if volumeMount.Name == volumeName {
tokenPath, err := getProjectedTokenPath(pod, volumeName)
if err != nil {
return nil, err
}
return &ServiceAccountTokenVolume{
Name: volumeMount.Name,
MountPath: volumeMount.MountPath,
TokenPath: tokenPath,
}, nil
}
}
}

// Otherwise, check the volume exists and fallback to `DefaultServiceAccountMount`
for _, volume := range pod.Spec.Volumes {
if volume.Name == volumeName {
tokenPath, err := getTokenPathFromProjectedVolume(volume)
if err != nil {
return nil, err
}
return &ServiceAccountTokenVolume{
Name: volume.Name,
MountPath: DefaultServiceAccountMount,
TokenPath: tokenPath,
}, nil
}
}

return nil, fmt.Errorf("failed to find service account volume %q", volumeName)
}

// Fallback to searching for normal service account token
for _, container := range pod.Spec.Containers {
for _, volumes := range container.VolumeMounts {
if strings.Contains(volumes.MountPath, "serviceaccount") {
return volumes.Name, volumes.MountPath
return &ServiceAccountTokenVolume{
Name: volumes.Name,
MountPath: volumes.MountPath,
TokenPath: "token",
}, nil
}
}
}

return nil, fmt.Errorf("failed to find service account volume mount")
}

// getProjectedTokenPath searches through a Pod's Volumes for volumeName, and
// attempts to retrieve the projected token path from that volume
func getProjectedTokenPath(pod *corev1.Pod, volumeName string) (string, error) {
for _, volume := range pod.Spec.Volumes {
if volume.Name == volumeName {
return getTokenPathFromProjectedVolume(volume)
}
}
return "", fmt.Errorf("failed to find volume %q in Pod %q volumes", volumeName, pod.Name)
}

func getTokenPathFromProjectedVolume(volume corev1.Volume) (string, error) {
if volume.Projected != nil {
for _, source := range volume.Projected.Sources {
if source.ServiceAccountToken != nil && source.ServiceAccountToken.Path != "" {
return source.ServiceAccountToken.Path, nil
}
}
}
return serviceAccountName, serviceAccountPath
return "", fmt.Errorf("failed to find tokenPath for projected volume %q", volume.Name)
}

// IRSA support - get aws_iam_token volume mount details to inject to vault containers
Expand Down
Loading