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

Added support for IRSA, fixes - #86 #169

Merged
merged 8 commits into from
Apr 13, 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
74 changes: 58 additions & 16 deletions agent-inject/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ type Agent struct {
// which can be referenced by the Agent config for secrets. Mounted at /vault/custom/
ExtraSecret string

// AwsIamTokenAccountName is the aws iam volume mount name for the pod.
// Need this for IRSA aka pod identity
AwsIamTokenAccountName string

// AwsIamTokenAccountPath is the aws iam volume mount path for the pod
// where the JWT would be present
// Need this for IRSA aka pod identity
AwsIamTokenAccountPath string
// CopyVolumeMounts is the name of the container in the Pod whose volume mounts
// should be copied into the Vault Agent init and/or sidecar containers.
CopyVolumeMounts string
Expand Down Expand Up @@ -250,24 +258,30 @@ type VaultAgentCache 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)
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],
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,
Vault: Vault{
Address: pod.Annotations[AnnotationVaultService],
ProxyAddress: pod.Annotations[AnnotationProxyAddress],
Expand Down Expand Up @@ -588,6 +602,34 @@ func serviceaccount(pod *corev1.Pod) (string, string) {
return serviceAccountName, serviceAccountPath
}

// IRSA support - get aws_iam_token volume mount details to inject to vault containers
func getAwsIamTokenVolume(pod *corev1.Pod) (string, string) {
var awsIamTokenAccountName, awsIamTokenAccountPath string
for _, container := range pod.Spec.Containers {
for _, volumes := range container.VolumeMounts {
if strings.Contains(volumes.MountPath, "eks.amazonaws.com") {
return volumes.Name, volumes.MountPath
}
}
}
return awsIamTokenAccountName, awsIamTokenAccountPath
}

// IRSA support - get aws envs to inject to vault containers
func (a *Agent) getAwsEnvsFromContainer(pod *corev1.Pod) map[string]string {
envMap := make(map[string]string)
for _, container := range pod.Spec.Containers {
for _, env := range container.Env {
if env.Name == "AWS_ROLE_ARN" || env.Name == "AWS_WEB_IDENTITY_TOKEN_FILE" {
if _, ok := envMap[env.Name]; !ok {
envMap[env.Name] = env.Value
}
}
}
}
return envMap
}

func (a *Agent) vaultCliFlags() []string {
flags := []string{
fmt.Sprintf("-address=%s", a.Vault.Address),
Expand Down
31 changes: 31 additions & 0 deletions agent-inject/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,37 @@ func testPod(annotations map[string]string) *corev1.Pod {
}
}

func testPodIRSA(annotations map[string]string) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Annotations: annotations,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "foobar",
VolumeMounts: []corev1.VolumeMount{
{
Name: "foobar",
MountPath: "serviceaccount/somewhere",
},
},
},
{
Name: "aws-iam-token",
VolumeMounts: []corev1.VolumeMount{
{
Name: "aws-iam-token",
MountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount",
},
},
},
},
},
}
}

func TestShouldInject(t *testing.T) {
tests := []struct {
annotations map[string]string
Expand Down
12 changes: 11 additions & 1 deletion agent-inject/agent/container_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package agent

import (
"encoding/base64"

corev1 "k8s.io/api/core/v1"
)

Expand Down Expand Up @@ -59,5 +58,16 @@ func (a *Agent) ContainerEnvVars(init bool) ([]corev1.EnvVar, error) {
})
}

// Add IRSA AWS Env variables for vault containers
if a.Vault.AuthType == "aws" {
envMap := a.getAwsEnvsFromContainer(a.Pod)
for k, v := range envMap {
envs = append(envs, corev1.EnvVar{
Name: k,
Value: v,
})
}
}

return envs, nil
}
57 changes: 56 additions & 1 deletion agent-inject/agent/container_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"

"github.com/hashicorp/vault/sdk/helper/strutil"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestContainerEnvs(t *testing.T) {
Expand All @@ -25,7 +27,6 @@ func TestContainerEnvs(t *testing.T) {
if err != nil {
t.Errorf("got error, shouldn't have: %s", err)
}

if len(envs) != len(tt.expectedEnvs) {
t.Errorf("number of envs mismatch, wanted %d, got %d", len(tt.expectedEnvs), len(envs))
}
Expand All @@ -37,3 +38,57 @@ func TestContainerEnvs(t *testing.T) {
}
}
}

func TestContainerEnvsForIRSA(t *testing.T) {
envTests := []struct {
agent Agent
expectedEnvs []string
}{
{Agent{Pod: testPodWithoutIRSA()}, []string{"VAULT_CONFIG"}},
{Agent{Pod: testPodWithIRSA(), Vault: Vault{AuthType: "aws",}},
[]string{"VAULT_CONFIG", "AWS_ROLE_ARN", "AWS_WEB_IDENTITY_TOKEN_FILE"},
},
}
for _, tt := range envTests {
envs, err := tt.agent.ContainerEnvVars(true)
if err != nil {
t.Errorf("got error, shouldn't have: %s", err)
}
if len(envs) != len(tt.expectedEnvs) {
t.Errorf("number of envs mismatch, wanted %d, got %d", len(tt.expectedEnvs), len(envs))
}
}
}

func testPodWithoutIRSA() *corev1.Pod {
return testPodWithEnv(nil)
}

func testPodWithIRSA() *corev1.Pod {
return testPodWithEnv([]corev1.EnvVar{
{
Name: "AWS_ROLE_ARN",
Value: "foorole",
},
{
Name: "AWS_WEB_IDENTITY_TOKEN_FILE",
Value: "footoken",
},
})
}

func testPodWithEnv(envVars []corev1.EnvVar) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "foobar",
Env: envVars,
},
},
},
}
}
7 changes: 7 additions & 0 deletions agent-inject/agent/container_init_sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ func (a *Agent) ContainerInitSidecar() (corev1.Container, error) {
ReadOnly: true,
},
}
if a.AwsIamTokenAccountName != "" && a.AwsIamTokenAccountPath != "" {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: a.AwsIamTokenAccountName,
MountPath: a.AwsIamTokenAccountPath,
ReadOnly: true,
})
}
volumeMounts = append(volumeMounts, a.ContainerVolumeMounts()...)

if a.ExtraSecret != "" {
Expand Down
7 changes: 7 additions & 0 deletions agent-inject/agent/container_sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ func (a *Agent) ContainerSidecar() (corev1.Container, error) {
ReadOnly: false,
},
}
if a.AwsIamTokenAccountName != "" && a.AwsIamTokenAccountPath != "" {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: a.AwsIamTokenAccountName,
MountPath: a.AwsIamTokenAccountPath,
ReadOnly: true,
})
}
volumeMounts = append(volumeMounts, a.ContainerVolumeMounts()...)

if a.ExtraSecret != "" {
Expand Down
88 changes: 82 additions & 6 deletions agent-inject/agent/container_sidecar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,32 @@ func TestContainerSidecarVolume(t *testing.T) {
require.Equal(
t,
[]corev1.VolumeMount{
corev1.VolumeMount{
{
Name: agent.ServiceAccountName,
MountPath: agent.ServiceAccountPath,
ReadOnly: true,
},
corev1.VolumeMount{
{
Name: tokenVolumeNameSidecar,
MountPath: tokenVolumePath,
ReadOnly: false,
},
corev1.VolumeMount{
{
Name: secretVolumeName,
MountPath: agent.Annotations[AnnotationVaultSecretVolumePath],
ReadOnly: false,
},
corev1.VolumeMount{
{
Name: fmt.Sprintf("%s-custom-%d", secretVolumeName, 0),
MountPath: "/etc/container_environment",
ReadOnly: false,
},
corev1.VolumeMount{
{
Name: extraSecretVolumeName,
MountPath: extraSecretVolumePath,
ReadOnly: true,
},
corev1.VolumeMount{
{
Name: "tobecopied",
MountPath: "/etc/somewhereelse",
ReadOnly: false,
Expand All @@ -99,6 +99,82 @@ func TestContainerSidecarVolume(t *testing.T) {
)
}

func TestContainerSidecarVolumeWithIRSA(t *testing.T) {

annotations := map[string]string{
AnnotationVaultRole: "foobar",
// this will have different mount path
fmt.Sprintf("%s-%s", AnnotationAgentInjectSecret, "secret1"): "secrets/secret1",
fmt.Sprintf("%s-%s", AnnotationVaultSecretVolumePath, "secret1"): "/etc/container_environment",

// this secret will have same mount path as default mount path
// adding this so we can make sure we don't have duplicate
// volume mounts
fmt.Sprintf("%s-%s", AnnotationAgentInjectSecret, "secret2"): "secret/secret2",
fmt.Sprintf("%s-%s", AnnotationVaultSecretVolumePath, "secret2"): "/etc/default_path",

// Default path for all secrets
AnnotationVaultSecretVolumePath: "/etc/default_path",

fmt.Sprintf("%s-%s", AnnotationAgentInjectSecret, "secret3"): "secret/secret3",
}

pod := testPodIRSA(annotations)
var patches []*jsonpatch.JsonPatchOperation

err := Init(pod, AgentConfig{
"foobar-image", "http://foobar:1234", "aws", "test", "test", true, "1000", "100",
DefaultAgentRunAsSameUser, DefaultAgentSetSecurityContext, "", "map",
DefaultResourceRequestCPU, DefaultResourceRequestMem, DefaultResourceLimitCPU, DefaultResourceLimitMem})
if err != nil {
t.Errorf("got error, shouldn't have: %s", err)
}

agent, err := New(pod, patches)
infa-mhadiman marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
assert.Equal(t, "aws-iam-token", agent.AwsIamTokenAccountName)
assert.Equal(t, "/var/run/secrets/eks.amazonaws.com/serviceaccount", agent.AwsIamTokenAccountPath)

if err := agent.Validate(); err != nil {
t.Errorf("agent validation failed, it shouldn't have: %s", err)
}

container, err := agent.ContainerSidecar()
infa-mhadiman marked this conversation as resolved.
Show resolved Hide resolved
require.NoError(t, err)
// One token volume mount, one config volume mount and two secrets volume mounts
require.Equal(
t,
[]corev1.VolumeMount{
{
Name: agent.ServiceAccountName,
MountPath: agent.ServiceAccountPath,
ReadOnly: true,
},
{
Name: tokenVolumeNameSidecar,
MountPath: tokenVolumePath,
ReadOnly: false,
},
{
Name: agent.AwsIamTokenAccountName,
MountPath: agent.AwsIamTokenAccountPath,
ReadOnly: true,
},
{
Name: secretVolumeName,
MountPath: agent.Annotations[AnnotationVaultSecretVolumePath],
ReadOnly: false,
},
{
Name: fmt.Sprintf("%s-custom-%d", secretVolumeName, 0),
MountPath: "/etc/container_environment",
ReadOnly: false,
},
},
container.VolumeMounts,
)
}

func TestContainerSidecar(t *testing.T) {
annotations := map[string]string{
AnnotationVaultRole: "foobar",
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
Expand Down