From fe57e687694a99a6cedc2101e418916aa5efa383 Mon Sep 17 00:00:00 2001 From: jillianwilson Date: Sun, 31 Oct 2021 20:57:58 -0300 Subject: [PATCH] Code cleanup and readme --- README.md | 6 +- secret-injector/README.md | 147 ++++++++++++++++--------- secret-injector/cmd/main.go | 6 +- secret-injector/pkg/webhook/webhook.go | 63 ++++++++--- 4 files changed, 150 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 4c879cb..3ff965f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ The 1Password Connect Kubernetes Operator will continually check for updates fro [Click here for more details on the 1Password Kubernetes Operator](operator/README.md) -## 1Password Secret Injector +## 1Password Secrets Injector for Kubernetes -[Click here for more details on the 1Password Secret Injector](secret-injector/README.md) +The 1Password Secrets Injector for Kubernetes provides the ability to integrate Kubernetes with 1Password. The 1Password Secrets Injector implements a mutating webhook to inject 1Password secrets as environment variables into a pod or deployment. This differs from the secert creation provided by the 1Password Kubernetes operator in that a Kubernetes Secret will not be created when injecting a secret into your resource. + +[Click here for more details on the 1Password Secrets Injector for Kubernetes](secret-injector/README.md) # Security diff --git a/secret-injector/README.md b/secret-injector/README.md index 46b4f4a..bb8a3f1 100644 --- a/secret-injector/README.md +++ b/secret-injector/README.md @@ -1,78 +1,125 @@ -## Deploy +# 1Password Secrets Injector for Kubernetes +The 1Password Secrets Injector for Kubernetes provides the ability to integrate Kubernetes with 1Password. The 1Password Secrets Injector implements a mutating webhook to inject 1Password secrets as environment variables into a pod or deployment. This differs from the secert creation provided by the 1Password Kubernetes operator in that a Kubernetes Secret will not be created when injecting a secret into your resource. -1. Create namespace `op-secret-injector` in which the 1Password secret injector webhook is deployed: +## Use with the 1Password Kubernetes Operator +The 1Password Secrets Injector for Kubernetes can be used in conjuction with the 1Password Kubernetes Operator in order to provide automatic deployment restarts when a 1Password item being used by your deployment has been updated. + + +[Click here for more details on the 1Password Kubernetes Operator](operator/README.md) + +## Setup and Deployment + +The 1Password Secrets Injector for Kubernetes uses a webhook server in order to inject secrets into pods and deployments. Admission to the webhook server is needs to be s secure operation, thus communication with the webhook server requires a TLS certificate signed by a Kubernetes CA. + +For a simple setup we suggest using s script by morvencao for [creating a signed cert for the webook](https://github.com/morvencao/kube-mutating-webhook-tutorial/blob/master/deploy/webhook-create-signed-cert.sh). A copy of this script can also be found in this repo [here](secret-injector/deploy/webhook-create-signed-cert.sh). + +Run the script with the following: +``` +./deploy/webhook-create-signed-cert.sh \ + --service \ + --secret \ + --namespace +``` +This will genrate a Kubernetes Secret with your signed certificate. + +Next we must set the webhook configuration. An example of this configuration can be found [here](secret-injector/deploy/mutatingwebhook.sh). If you choose to use this example, replace `${CA_BUNDLE}` file's with the value stored for `client-ca-file` in the Kubernetes Secret you generated in the previous step. ``` -# kubectl create ns op-secret-injector +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: op-secret-injector-webhook-cfg + labels: + app: op-secret-injector +webhooks: +- name: op-secret-injector.morven.me + clientConfig: + service: + name: op-secret-injector-webhook-svc + namespace: op-secret-injector + path: "/inject" + caBundle: ${CA_BUNDLE} //replace this with your own CA Bundle + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + namespaceSelector: + matchLabels: + op-secret-injection: enabled ``` -2. Create a signed cert/key pair and store it in a Kubernetes `secret` that will be consumed by 1Password secret injector deployment: +You can automate this step using the script by [morvencao](https://github.com/morvencao/kube-mutating-webhook-tutorial). ``` -# ./deploy/webhook-create-signed-cert.sh \ - --service op-secret-injector-webhook-svc \ - --secret op-secret-injector-webhook-certs \ - --namespace op-secret-injector -``` - -3. Patch the `MutatingWebhookConfiguration` by set `caBundle` with correct value from Kubernetes cluster: - -``` -# cat deploy/mutatingwebhook.yaml | \ +cat deploy/mutatingwebhook.yaml | \ deploy/webhook-patch-ca-bundle.sh > \ deploy/mutatingwebhook-ca-bundle.yaml ``` -4. Deploy resources: +Lastly, we must apply the deployment, service, and mutating webhook configuration to kubernetes: ``` -# kubectl create -f deploy/deployment.yaml -# kubectl create -f deploy/service.yaml -# kubectl create -f deploy/mutatingwebhook-ca-bundle.yaml +kubectl create -f deploy/deployment.yaml +kubectl create -f deploy/service.yaml +kubectl create -f deploy/mutatingwebhook-ca-bundle.yaml ``` -## Verify +## Usage -1. The sidecar inject webhook should be in running state: +For every namespace you want the 1Password Secret Injector to inject secrets for, you must add the label `sidecar-injector=enabled` label to the namespace: ``` -# kubectl -n sidecar-injector get pod -NAME READY STATUS RESTARTS AGE -sidecar-injector-webhook-deployment-7c8bc5f4c9-28c84 1/1 Running 0 30s -# kubectl -n sidecar-injector get deploy -NAME READY UP-TO-DATE AVAILABLE AGE -sidecar-injector-webhook-deployment 1/1 1 1 67s +kubectl label namespace injection sidecar-injection=enabled ``` -2. Create new namespace `injection` and label it with `sidecar-injector=enabled`: +To inject a 1Password secret as an environment variable, your pod or deployment you must add an environment variable to the resource with a value referencing your 1Password item in the format `op:///[/section]/`. You must also annotate your pod/deployment spec with `operator.1password.io/inject` which expects a comma separated list of the names of the containers to that will be mutated and have secrets injected. ``` -# kubectl create ns injection -# kubectl label namespace injection sidecar-injection=enabled -# kubectl get namespace -L sidecar-injection -NAME STATUS AGE SIDECAR-INJECTION -default Active 26m -injection Active 13s enabled -kube-public Active 26m -kube-system Active 26m -sidecar-injector Active 17m +#example + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app-example +spec: + selector: + matchLabels: + app: app-example + template: + metadata: + annotations: + operator.1password.io/inject: "app-example,another-example" + labels: + app: app-example + spec: + containers: + - name: app-example + image: my-image + env: + - name: DB_USERNAME + value: op://my-vault/my-item/sql/username + - name: DB_PASSWORD + value: op://my-vault/my-item/sql/password + - name: another-example + image: my-image + env: + - name: DB_USERNAME + value: op://my-vault/my-item/sql/username + - name: DB_PASSWORD + value: op://my-vault/my-item/sql/password + - name: my-app //because my-app is not listed in the inject annotaion above the environment values for this container will not be updated with secret values + image: my-image + env: + - name: DB_USERNAME + value: op://my-vault/my-item/sql/username + - name: DB_PASSWORD + value: op://my-vault/my-item/sql/password ``` -3. Deploy an app in Kubernetes cluster, take `alpine` app as an example +## Attributions -``` -# kubectl run alpine --image=alpine --restart=Never -n injection --overrides='{"apiVersion":"v1","metadata":{"annotations":{"sidecar-injector-webhook.morven.me/inject":"yes"}}}' --command -- sleep infinity -``` - -4. Verify sidecar container is injected: - -``` -# kubectl get pod -NAME READY STATUS RESTARTS AGE -alpine 2/2 Running 0 1m -# kubectl -n injection get pod alpine -o jsonpath="{.spec.containers[*].name}" -alpine sidecar-nginx -``` +This project is based on and heavily inspired by [morvencao's Kubernetes Mutating Webhook for Sidecar Injection tutorial](https://github.com/morvencao/kube-mutating-webhook-tutorial). ## Troubleshooting diff --git a/secret-injector/cmd/main.go b/secret-injector/cmd/main.go index 522962f..480edb5 100644 --- a/secret-injector/cmd/main.go +++ b/secret-injector/cmd/main.go @@ -37,17 +37,17 @@ func main() { connectHost, present := os.LookupEnv(connectHostEnv) if !present { - glog.Error("") + glog.Error("Connect host not set") } connectTokenName, present := os.LookupEnv(connectTokenSecretNameEnv) if !present { - glog.Error("") + glog.Error("Connect token name not set") } connectTokenKey, present := os.LookupEnv(connectTokenSecretKeyEnv) if !present { - glog.Error("") + glog.Error("Connect token key not set") } webhookConfig := webhook.Config{ diff --git a/secret-injector/pkg/webhook/webhook.go b/secret-injector/pkg/webhook/webhook.go index 608fcab..d493b17 100644 --- a/secret-injector/pkg/webhook/webhook.go +++ b/secret-injector/pkg/webhook/webhook.go @@ -24,6 +24,9 @@ const ( // binVolumeMountPath is the mount path where the OP CLI binary can be found. binVolumeMountPath = "/op/bin/" + + connectTokenEnv = "OP_CONNECT_TOKEN" + connectHostEnv = "OP_CONNECT_HOST" ) // binVolume is the shared, in-memory volume where the OP CLI binary lives. @@ -281,7 +284,7 @@ func (whsvr *WebhookServer) mutate(ar *v1beta1.AdmissionReview) *v1beta1.Admissi // into a shared volume mount. var binInitContainer = corev1.Container{ Name: "copy-op-bin", - Image: "op-example" + ":" + version, + Image: "1password/op" + ":" + version, ImagePullPolicy: corev1.PullIfNotPresent, Command: []string{"sh", "-c", fmt.Sprintf("cp /usr/local/bin/op %s", binVolumeMountPath)}, @@ -332,26 +335,31 @@ func createOPCLIPatch(pod *corev1.Pod, annotations map[string]string, containers func createOPConnectPatch(container *corev1.Container, containerIndex int, host, tokenSecretName, tokenSecretKey string) []patchOperation { var patch []patchOperation - connectHostEnvVar := corev1.EnvVar{ - Name: "OP_CONNECT_HOST", - Value: host, + envs := []corev1.EnvVar{} + + hostConfig, tokenConfig := isConnectConfigurationSet(container) + + if hostConfig { + connectHostEnvVar := corev1.EnvVar{ + Name: "OP_CONNECT_HOST", + Value: host, + } + envs = append(envs, connectHostEnvVar) } - connectTokenEnvVar := corev1.EnvVar{ - Name: "OP_CONNECT_TOKEN", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - Key: tokenSecretKey, - LocalObjectReference: corev1.LocalObjectReference{ - Name: tokenSecretName, + if tokenConfig { + connectTokenEnvVar := corev1.EnvVar{ + Name: "OP_CONNECT_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: tokenSecretKey, + LocalObjectReference: corev1.LocalObjectReference{ + Name: tokenSecretName, + }, }, }, - }, - } - - envs := []corev1.EnvVar{ - connectHostEnvVar, - connectTokenEnvVar, + } + envs = append(envs, connectTokenEnvVar) } patch = append(patch, setEnvironment(*container, containerIndex, envs, "/spec/containers")...) @@ -359,6 +367,27 @@ func createOPConnectPatch(container *corev1.Container, containerIndex int, host, return patch } +func isConnectConfigurationSet(container *corev1.Container) (bool, bool) { + + hostConfig := false + tokenConfig := false + + for _, env := range container.Env { + if env.Name == connectHostEnv { + hostConfig = true + } + + if env.Name == connectTokenEnv { + tokenConfig = true + } + + if tokenConfig && hostConfig { + break + } + } + return hostConfig, tokenConfig +} + func (whsvr *WebhookServer) mutateContainer(_ context.Context, container *corev1.Container, containerIndex int) (*corev1.Container, bool, []patchOperation, error) { // Because we are running a command in the pod before starting the container app, // we need to prepend the pod comand with the op run command