From a3db36698acfc430d4223d3d4d3adf01a8b32f0b Mon Sep 17 00:00:00 2001 From: joe miller Date: Sat, 25 Jan 2020 09:08:32 -0800 Subject: [PATCH] allow agent container RunAsUser/Group to be specified via annotations --- agent-inject/agent/agent.go | 22 ++++- agent-inject/agent/annotations.go | 14 +++ agent-inject/agent/annotations_test.go | 38 ++++++++- agent-inject/agent/container_init_sidecar.go | 22 ++--- agent-inject/agent/container_sidecar.go | 33 ++++--- agent-inject/agent/container_sidecar_test.go | 90 ++++++++++++++++++++ 6 files changed, 191 insertions(+), 28 deletions(-) diff --git a/agent-inject/agent/agent.go b/agent-inject/agent/agent.go index 2c294a6e..49ece399 100644 --- a/agent-inject/agent/agent.go +++ b/agent-inject/agent/agent.go @@ -14,8 +14,10 @@ import ( // TODO swap out 'github.com/mattbaird/jsonpatch' for 'github.com/evanphx/json-patch' const ( - DefaultVaultImage = "vault:1.3.1" - DefaultVaultAuthPath = "auth/kubernetes" + DefaultVaultImage = "vault:1.3.1" + DefaultVaultAuthPath = "auth/kubernetes" + DefaultAgentRunAsUser = 100 + DefaultAgentRunAsGroup = 1000 ) // Agent is the top level structure holding all the @@ -85,6 +87,12 @@ type Agent struct { // Vault is the structure holding all the Vault specific configurations. Vault Vault + + // RunAsUser is the user ID to run the Vault agent container(s) as. + RunAsUser int64 + + // RunAsGroup is the group ID to run the Vault agent container(s) as. + RunAsGroup int64 } type Secret struct { @@ -202,6 +210,16 @@ func New(pod *corev1.Pod, patches []*jsonpatch.JsonPatchOperation) (*Agent, erro return agent, err } + agent.RunAsUser, err = strconv.ParseInt(pod.Annotations[AnnotationAgentRunAsUser], 10, 64) + if err != nil { + return agent, err + } + + agent.RunAsGroup, err = strconv.ParseInt(pod.Annotations[AnnotationAgentRunAsGroup], 10, 64) + if err != nil { + return agent, err + } + return agent, nil } diff --git a/agent-inject/agent/annotations.go b/agent-inject/agent/annotations.go index eb618508..fae03d1d 100644 --- a/agent-inject/agent/annotations.go +++ b/agent-inject/agent/annotations.go @@ -79,6 +79,12 @@ const ( // AnnotationAgentRequestsMem sets the requested memory amount on the Vault Agent containers. AnnotationAgentRequestsMem = "vault.hashicorp.com/agent-requests-mem" + // AnnotationAgentRunAsUser sets the User ID to run the Vault Agent containers as. + AnnotationAgentRunAsUser = "vault.hashicorp.com/agent-run-as-user" + + // AnnotationAgentRunAsGroup sets the Group ID to run the Vault Agent containers as. + AnnotationAgentRunAsGroup = "vault.hashicorp.com/agent-run-as-group" + // AnnotationVaultService is the name of the Vault server. This can be overridden by the // user but will be set by a flag on the deployment. AnnotationVaultService = "vault.hashicorp.com/service" @@ -184,6 +190,14 @@ func Init(pod *corev1.Pod, image, address, authPath, namespace string) error { pod.ObjectMeta.Annotations[AnnotationAgentRequestsMem] = DefaultResourceRequestMem } + if _, ok := pod.ObjectMeta.Annotations[AnnotationAgentRunAsUser]; !ok { + pod.ObjectMeta.Annotations[AnnotationAgentRunAsUser] = strconv.Itoa(DefaultAgentRunAsUser) + } + + if _, ok := pod.ObjectMeta.Annotations[AnnotationAgentRunAsGroup]; !ok { + pod.ObjectMeta.Annotations[AnnotationAgentRunAsGroup] = strconv.Itoa(DefaultAgentRunAsGroup) + } + return nil } diff --git a/agent-inject/agent/annotations_test.go b/agent-inject/agent/annotations_test.go index 14fe60d6..1b779815 100644 --- a/agent-inject/agent/annotations_test.go +++ b/agent-inject/agent/annotations_test.go @@ -2,6 +2,7 @@ package agent import ( "reflect" + "strconv" "strings" "testing" @@ -55,6 +56,8 @@ func TestInitDefaults(t *testing.T) { annotationValue string }{ {annotationKey: AnnotationAgentImage, annotationValue: DefaultVaultImage}, + {annotationKey: AnnotationAgentRunAsUser, annotationValue: strconv.Itoa(DefaultAgentRunAsUser)}, + {annotationKey: AnnotationAgentRunAsGroup, annotationValue: strconv.Itoa(DefaultAgentRunAsGroup)}, } for _, tt := range tests { @@ -127,6 +130,11 @@ func TestSecretAnnotations(t *testing.T) { pod := testPod(annotation) var patches []*jsonpatch.JsonPatchOperation + err := Init(pod, "", "http://foobar:8200", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + agent, err := New(pod, patches) if err != nil { t.Errorf("got error, shouldn't have: %s", err) @@ -200,6 +208,11 @@ func TestSecretTemplateAnnotations(t *testing.T) { pod := testPod(tt.annotations) var patches []*jsonpatch.JsonPatchOperation + err := Init(pod, "", "http://foobar:8200", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + agent, err := New(pod, patches) if err != nil { t.Errorf("got error, shouldn't have: %s", err) @@ -250,6 +263,11 @@ func TestTemplateShortcuts(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pod := testPod(tt.annotations) + err := Init(pod, "", "http://foobar:8200", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + var patches []*jsonpatch.JsonPatchOperation agent, err := New(pod, patches) @@ -301,6 +319,11 @@ func TestSecretCommandAnnotations(t *testing.T) { for _, tt := range tests { pod := testPod(tt.annotations) + err := Init(pod, "", "http://foobar:8200", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + var patches []*jsonpatch.JsonPatchOperation agent, err := New(pod, patches) @@ -375,6 +398,14 @@ func TestCouldErrorAnnotations(t *testing.T) { {AnnotationVaultTLSSkipVerify, "tRuE", false}, {AnnotationVaultTLSSkipVerify, "fAlSe", false}, {AnnotationVaultTLSSkipVerify, "", false}, + + {AnnotationAgentRunAsUser, "0", true}, + {AnnotationAgentRunAsUser, "100", true}, + {AnnotationAgentRunAsUser, "root", false}, + + {AnnotationAgentRunAsGroup, "0", true}, + {AnnotationAgentRunAsGroup, "100", true}, + {AnnotationAgentRunAsGroup, "root", false}, } for i, tt := range tests { @@ -382,7 +413,12 @@ func TestCouldErrorAnnotations(t *testing.T) { pod := testPod(annotations) var patches []*jsonpatch.JsonPatchOperation - _, err := New(pod, patches) + err := Init(pod, "", "http://foobar:8200", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + _, err = New(pod, patches) if err != nil && tt.valid { t.Errorf("[%d] got error, shouldn't have: %s", i, err) } else if err == nil && !tt.valid { diff --git a/agent-inject/agent/container_init_sidecar.go b/agent-inject/agent/container_init_sidecar.go index 91be0978..730d8cce 100644 --- a/agent-inject/agent/container_init_sidecar.go +++ b/agent-inject/agent/container_init_sidecar.go @@ -2,7 +2,7 @@ package agent import ( "fmt" - "github.com/hashicorp/vault/sdk/helper/pointerutil" + corev1 "k8s.io/api/core/v1" ) @@ -55,17 +55,13 @@ func (a *Agent) ContainerInitSidecar() (corev1.Container, error) { } return corev1.Container{ - Name: "vault-agent-init", - Image: a.ImageName, - Env: envs, - Resources: resources, - SecurityContext: &corev1.SecurityContext{ - RunAsUser: pointerutil.Int64Ptr(100), - RunAsGroup: pointerutil.Int64Ptr(1000), - RunAsNonRoot: pointerutil.BoolPtr(true), - }, - VolumeMounts: volumeMounts, - Command: []string{"/bin/sh", "-ec"}, - Args: []string{arg}, + Name: "vault-agent-init", + Image: a.ImageName, + Env: envs, + Resources: resources, + SecurityContext: a.securityContext(), + VolumeMounts: volumeMounts, + Command: []string{"/bin/sh", "-ec"}, + Args: []string{arg}, }, nil } diff --git a/agent-inject/agent/container_sidecar.go b/agent-inject/agent/container_sidecar.go index f3955e03..a77c4a3c 100644 --- a/agent-inject/agent/container_sidecar.go +++ b/agent-inject/agent/container_sidecar.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "k8s.io/apimachinery/pkg/api/resource" "github.com/hashicorp/vault/sdk/helper/pointerutil" @@ -63,18 +64,14 @@ func (a *Agent) ContainerSidecar() (corev1.Container, error) { } return corev1.Container{ - Name: "vault-agent", - Image: a.ImageName, - Env: envs, - Resources: resources, - SecurityContext: &corev1.SecurityContext{ - RunAsUser: pointerutil.Int64Ptr(100), - RunAsGroup: pointerutil.Int64Ptr(1000), - RunAsNonRoot: pointerutil.BoolPtr(true), - }, - VolumeMounts: volumeMounts, - Command: []string{"/bin/sh", "-ec"}, - Args: []string{arg}, + Name: "vault-agent", + Image: a.ImageName, + Env: envs, + Resources: resources, + SecurityContext: a.securityContext(), + VolumeMounts: volumeMounts, + Command: []string{"/bin/sh", "-ec"}, + Args: []string{arg}, }, nil } @@ -124,3 +121,15 @@ func parseQuantity(raw string) (resource.Quantity, error) { return resource.ParseQuantity(raw) } + +func (a *Agent) securityContext() *corev1.SecurityContext { + runAsNonRoot := true + if a.RunAsUser == 0 || a.RunAsGroup == 0 { + runAsNonRoot = false + } + return &corev1.SecurityContext{ + RunAsUser: pointerutil.Int64Ptr(a.RunAsUser), + RunAsGroup: pointerutil.Int64Ptr(a.RunAsGroup), + RunAsNonRoot: pointerutil.BoolPtr(runAsNonRoot), + } +} diff --git a/agent-inject/agent/container_sidecar_test.go b/agent-inject/agent/container_sidecar_test.go index 3cd6a436..ded48e71 100644 --- a/agent-inject/agent/container_sidecar_test.go +++ b/agent-inject/agent/container_sidecar_test.go @@ -2,6 +2,7 @@ package agent import ( "fmt" + "strconv" "testing" "github.com/mattbaird/jsonpatch" @@ -385,3 +386,92 @@ func TestContainerSidecarCustomResources(t *testing.T) { }) } } + +func TestContainerSidecarSecurityContext(t *testing.T) { + tests := []struct { + name string + runAsUser int + runAsGroup int + expectedRunAsUser int64 + expectedRunAsGroup int64 + expectedRunAsNonRoot bool + }{ + { + name: "Defaults", + runAsUser: DefaultAgentRunAsUser, + runAsGroup: DefaultAgentRunAsGroup, + expectedRunAsUser: DefaultAgentRunAsUser, + expectedRunAsGroup: DefaultAgentRunAsGroup, + expectedRunAsNonRoot: true, + }, + { + name: "non-root user and non-root group", + runAsUser: 1001, + runAsGroup: 1001, + expectedRunAsUser: 1001, + expectedRunAsGroup: 1001, + expectedRunAsNonRoot: true, + }, + { + name: "root user and group", + runAsUser: 0, + runAsGroup: 0, + expectedRunAsUser: 0, + expectedRunAsGroup: 0, + expectedRunAsNonRoot: false, + }, + { + name: "root user and non-root group", + runAsUser: 0, + runAsGroup: 100, + expectedRunAsUser: 0, + expectedRunAsGroup: 100, + expectedRunAsNonRoot: false, + }, + { + name: "non-root user and root group", + runAsUser: 100, + runAsGroup: 0, + expectedRunAsUser: 100, + expectedRunAsGroup: 0, + expectedRunAsNonRoot: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + annotations := map[string]string{ + AnnotationVaultRole: "foobar", + AnnotationAgentRunAsUser: strconv.Itoa(tt.runAsUser), + AnnotationAgentRunAsGroup: strconv.Itoa(tt.runAsGroup), + } + pod := testPod(annotations) + var patches []*jsonpatch.JsonPatchOperation + + err := Init(pod, "foobar-image", "http://foobar:1234", "test", "test") + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + agent, err := New(pod, patches) + if err := agent.Validate(); err != nil { + t.Errorf("agent validation failed, it shouldn't have: %s", err) + } + + container, err := agent.ContainerSidecar() + if err != nil { + t.Errorf("got error, shouldn't have: %s", err) + } + + if *container.SecurityContext.RunAsUser != tt.expectedRunAsUser { + t.Errorf("expected RunAsUser mismatch: wanted %d, got %d", tt.expectedRunAsUser, *container.SecurityContext.RunAsUser) + } + if *container.SecurityContext.RunAsGroup != tt.expectedRunAsGroup { + t.Errorf("expected RunAsGroup mismatch: wanted %d, got %d", tt.expectedRunAsGroup, *container.SecurityContext.RunAsGroup) + } + if *container.SecurityContext.RunAsNonRoot != tt.expectedRunAsNonRoot { + t.Errorf("expected RunAsNonRoot mismatch: wanted %t, got %t", tt.expectedRunAsNonRoot, *container.SecurityContext.RunAsNonRoot) + } + }) + } +}