Code cleanup and readme

This commit is contained in:
jillianwilson
2021-10-31 20:57:58 -03:00
parent 591b8949cd
commit fe57e68769
4 changed files with 150 additions and 72 deletions

View File

@@ -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) [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 # Security

View File

@@ -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 <name of webhook service> \
--secret <name of kubernetes secret where certificate will be stored> \
--namespace <your 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 \ cat deploy/mutatingwebhook.yaml | \
--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 | \
deploy/webhook-patch-ca-bundle.sh > \ deploy/webhook-patch-ca-bundle.sh > \
deploy/mutatingwebhook-ca-bundle.yaml 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/deployment.yaml
# kubectl create -f deploy/service.yaml kubectl create -f deploy/service.yaml
# kubectl create -f deploy/mutatingwebhook-ca-bundle.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 kubectl label namespace injection sidecar-injection=enabled
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
``` ```
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://<vault>/<item>[/section]/<field>`. 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 #example
# kubectl label namespace injection sidecar-injection=enabled
# kubectl get namespace -L sidecar-injection apiVersion: apps/v1
NAME STATUS AGE SIDECAR-INJECTION kind: Deployment
default Active 26m metadata:
injection Active 13s enabled name: app-example
kube-public Active 26m spec:
kube-system Active 26m selector:
sidecar-injector Active 17m 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
``` 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).
# 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
```
## Troubleshooting ## Troubleshooting

View File

@@ -37,17 +37,17 @@ func main() {
connectHost, present := os.LookupEnv(connectHostEnv) connectHost, present := os.LookupEnv(connectHostEnv)
if !present { if !present {
glog.Error("") glog.Error("Connect host not set")
} }
connectTokenName, present := os.LookupEnv(connectTokenSecretNameEnv) connectTokenName, present := os.LookupEnv(connectTokenSecretNameEnv)
if !present { if !present {
glog.Error("") glog.Error("Connect token name not set")
} }
connectTokenKey, present := os.LookupEnv(connectTokenSecretKeyEnv) connectTokenKey, present := os.LookupEnv(connectTokenSecretKeyEnv)
if !present { if !present {
glog.Error("") glog.Error("Connect token key not set")
} }
webhookConfig := webhook.Config{ webhookConfig := webhook.Config{

View File

@@ -24,6 +24,9 @@ const (
// binVolumeMountPath is the mount path where the OP CLI binary can be found. // binVolumeMountPath is the mount path where the OP CLI binary can be found.
binVolumeMountPath = "/op/bin/" binVolumeMountPath = "/op/bin/"
connectTokenEnv = "OP_CONNECT_TOKEN"
connectHostEnv = "OP_CONNECT_HOST"
) )
// binVolume is the shared, in-memory volume where the OP CLI binary lives. // 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. // into a shared volume mount.
var binInitContainer = corev1.Container{ var binInitContainer = corev1.Container{
Name: "copy-op-bin", Name: "copy-op-bin",
Image: "op-example" + ":" + version, Image: "1password/op" + ":" + version,
ImagePullPolicy: corev1.PullIfNotPresent, ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{"sh", "-c", Command: []string{"sh", "-c",
fmt.Sprintf("cp /usr/local/bin/op %s", binVolumeMountPath)}, 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 { func createOPConnectPatch(container *corev1.Container, containerIndex int, host, tokenSecretName, tokenSecretKey string) []patchOperation {
var patch []patchOperation var patch []patchOperation
connectHostEnvVar := corev1.EnvVar{ envs := []corev1.EnvVar{}
Name: "OP_CONNECT_HOST",
Value: host, hostConfig, tokenConfig := isConnectConfigurationSet(container)
if hostConfig {
connectHostEnvVar := corev1.EnvVar{
Name: "OP_CONNECT_HOST",
Value: host,
}
envs = append(envs, connectHostEnvVar)
} }
connectTokenEnvVar := corev1.EnvVar{ if tokenConfig {
Name: "OP_CONNECT_TOKEN", connectTokenEnvVar := corev1.EnvVar{
ValueFrom: &corev1.EnvVarSource{ Name: "OP_CONNECT_TOKEN",
SecretKeyRef: &corev1.SecretKeySelector{ ValueFrom: &corev1.EnvVarSource{
Key: tokenSecretKey, SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{ Key: tokenSecretKey,
Name: tokenSecretName, LocalObjectReference: corev1.LocalObjectReference{
Name: tokenSecretName,
},
}, },
}, },
}, }
} envs = append(envs, connectTokenEnvVar)
envs := []corev1.EnvVar{
connectHostEnvVar,
connectTokenEnvVar,
} }
patch = append(patch, setEnvironment(*container, containerIndex, envs, "/spec/containers")...) patch = append(patch, setEnvironment(*container, containerIndex, envs, "/spec/containers")...)
@@ -359,6 +367,27 @@ func createOPConnectPatch(container *corev1.Container, containerIndex int, host,
return patch 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) { 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, // 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 // we need to prepend the pod comand with the op run command