mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 07:28:06 +00:00
Code cleanup and readme
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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{
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user