mirror of
https://github.com/1Password/onepassword-operator.git
synced 2025-10-22 15:38:06 +00:00
Webhook that injects secrets into pods
This commit is contained in:
30
secret-injector/Dockerfile
Normal file
30
secret-injector/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# Build the manager binary
|
||||
FROM golang:1.13 as builder
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
|
||||
# Copy the go source
|
||||
COPY secret-injector/cmd/main.go secret-injector/main.go
|
||||
COPY secret-injector/pkg/ secret-injector/pkg/
|
||||
COPY vendor/ vendor/
|
||||
# Build
|
||||
ARG secret_injector_version=dev
|
||||
RUN CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
go build \
|
||||
-ldflags "-X \"github.com/1Password/onepassword-operator/operator/version.Version=$secret_injector_version\"" \
|
||||
-mod vendor \
|
||||
-a -o injector secret-injector/main.go
|
||||
|
||||
# Use distroless as minimal base image to package the secret-injector binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/injector .
|
||||
USER nonroot:nonroot
|
||||
|
||||
ENTRYPOINT ["/injector"]
|
||||
|
98
secret-injector/Makefile
Normal file
98
secret-injector/Makefile
Normal file
@@ -0,0 +1,98 @@
|
||||
# Image URL to use all building/pushing image targets;
|
||||
# Use your own docker registry and image name for dev/test by overridding the
|
||||
# IMAGE_REPO, IMAGE_NAME and IMAGE_TAG environment variable.
|
||||
IMAGE_REPO ?= docker.io/morvencao
|
||||
IMAGE_NAME ?= op-secret-injector
|
||||
|
||||
# Github host to use for checking the source tree;
|
||||
# Override this variable ue with your own value if you're working on forked repo.
|
||||
GIT_HOST ?= github.com/morvencao
|
||||
|
||||
PWD := $(shell pwd)
|
||||
BASE_DIR := $(shell basename $(PWD))
|
||||
|
||||
# Keep an existing GOPATH, make a private one if it is undefined
|
||||
GOPATH_DEFAULT := $(PWD)/.go
|
||||
export GOPATH ?= $(GOPATH_DEFAULT)
|
||||
TESTARGS_DEFAULT := "-v"
|
||||
export TESTARGS ?= $(TESTARGS_DEFAULT)
|
||||
DEST := $(GOPATH)/src/$(GIT_HOST)/$(BASE_DIR)
|
||||
IMAGE_TAG ?= $(shell date +v%Y%m%d)-$(shell git describe --match=$(git rev-parse --short=8 HEAD) --tags --always --dirty)
|
||||
|
||||
|
||||
LOCAL_OS := $(shell uname)
|
||||
ifeq ($(LOCAL_OS),Linux)
|
||||
TARGET_OS ?= linux
|
||||
XARGS_FLAGS="-r"
|
||||
else ifeq ($(LOCAL_OS),Darwin)
|
||||
TARGET_OS ?= darwin
|
||||
XARGS_FLAGS=
|
||||
else
|
||||
$(error "This system's OS $(LOCAL_OS) isn't recognized/supported")
|
||||
endif
|
||||
|
||||
all: fmt lint test build image
|
||||
|
||||
ifeq (,$(wildcard go.mod))
|
||||
ifneq ("$(realpath $(DEST))", "$(realpath $(PWD))")
|
||||
$(error Please run 'make' from $(DEST). Current directory is $(PWD))
|
||||
endif
|
||||
endif
|
||||
|
||||
############################################################
|
||||
# format section
|
||||
############################################################
|
||||
|
||||
fmt:
|
||||
@echo "Run go fmt..."
|
||||
|
||||
############################################################
|
||||
# lint section
|
||||
############################################################
|
||||
|
||||
lint:
|
||||
@echo "Runing the golangci-lint..."
|
||||
|
||||
############################################################
|
||||
# test section
|
||||
############################################################
|
||||
|
||||
test:
|
||||
@echo "Running the tests for $(IMAGE_NAME)..."
|
||||
@go test $(TESTARGS) ./...
|
||||
|
||||
############################################################
|
||||
# build section
|
||||
############################################################
|
||||
|
||||
build:
|
||||
@echo "Building the $(IMAGE_NAME) binary..."
|
||||
@CGO_ENABLED=0 go build -o build/_output/bin/$(IMAGE_NAME) ./cmd/
|
||||
|
||||
build-linux:
|
||||
@echo "Building the $(IMAGE_NAME) binary for Docker (linux)..."
|
||||
@GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/_output/linux/bin/$(IMAGE_NAME) ./cmd/
|
||||
|
||||
############################################################
|
||||
# image section
|
||||
############################################################
|
||||
|
||||
image: build-image push-image
|
||||
|
||||
build-image: build-linux
|
||||
@echo "Building the docker image: $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG)..."
|
||||
@docker build -t $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) -f build/Dockerfile .
|
||||
|
||||
push-image: build-image
|
||||
@echo "Pushing the docker image for $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) and $(IMAGE_REPO)/$(IMAGE_NAME):latest..."
|
||||
@docker tag $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO)/$(IMAGE_NAME):latest
|
||||
@docker push $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG)
|
||||
@docker push $(IMAGE_REPO)/$(IMAGE_NAME):latest
|
||||
|
||||
############################################################
|
||||
# clean section
|
||||
############################################################
|
||||
clean:
|
||||
@rm -rf build/_output
|
||||
|
||||
.PHONY: all fmt lint check test build image clean
|
84
secret-injector/README.md
Normal file
84
secret-injector/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
## Deploy
|
||||
|
||||
1. Create namespace `op-secret-injector` in which the 1Password secret injector webhook is deployed:
|
||||
|
||||
```
|
||||
# kubectl create ns op-secret-injector
|
||||
```
|
||||
|
||||
2. Create a signed cert/key pair and store it in a Kubernetes `secret` that will be consumed by 1Password secret injector deployment:
|
||||
|
||||
```
|
||||
# ./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 | \
|
||||
deploy/webhook-patch-ca-bundle.sh > \
|
||||
deploy/mutatingwebhook-ca-bundle.yaml
|
||||
```
|
||||
|
||||
4. Deploy resources:
|
||||
|
||||
```
|
||||
# kubectl create -f deploy/deployment.yaml
|
||||
# kubectl create -f deploy/service.yaml
|
||||
# kubectl create -f deploy/mutatingwebhook-ca-bundle.yaml
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
1. The sidecar inject webhook should be in running state:
|
||||
|
||||
```
|
||||
# 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
|
||||
```
|
||||
|
||||
2. Create new namespace `injection` and label it with `sidecar-injector=enabled`:
|
||||
|
||||
```
|
||||
# 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
|
||||
```
|
||||
|
||||
3. Deploy an app in Kubernetes cluster, take `alpine` app as an example
|
||||
|
||||
```
|
||||
# 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
|
||||
|
||||
Sometimes you may find that pod is injected with sidecar container as expected, check the following items:
|
||||
|
||||
1. The sidecar-injector webhook is in running state and no error logs.
|
||||
2. The namespace in which application pod is deployed has the correct labels as configured in `mutatingwebhookconfiguration`.
|
||||
3. Check the `caBundle` is patched to `mutatingwebhookconfiguration` object by checking if `caBundle` fields is empty.
|
||||
4. Check if the application pod has annotation `sidecar-injector-webhook.morven.me/inject":"yes"`.
|
52
secret-injector/app_example.yaml
Normal file
52
secret-injector/app_example.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: app-example
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: app-example
|
||||
ports:
|
||||
- port: 5000
|
||||
name: app-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"
|
||||
labels:
|
||||
app: app-example
|
||||
spec:
|
||||
containers:
|
||||
- name: app-example
|
||||
command: ["./example"]
|
||||
image: connect-app-example:latest
|
||||
imagePullPolicy: Never
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "0.2"
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
env:
|
||||
- name: OP_VAULT
|
||||
value: ApplicationConfiguration
|
||||
- name: APP_TITLE
|
||||
value: op://ApplicationConfiguration/Webapp/title
|
||||
- name: BUTTON_TEXT
|
||||
value: op://ApplicationConfiguration/Webapp/action
|
||||
- name: OP_CONNECT_HOST
|
||||
value: http://onepassword-connect:8080/
|
||||
- name: OP_CONNECT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: onepassword-token
|
||||
key: token
|
85
secret-injector/cmd/main.go
Normal file
85
secret-injector/cmd/main.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/1Password/onepassword-operator/secret-injector/pkg/webhook"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
connectTokenSecretKeyEnv = "OP_CONNECT_TOKEN_KEY"
|
||||
connectTokenSecretNameEnv = "OP_CONNECT_TOKEN_NAME"
|
||||
connectHostEnv = "OP_CONNECT_HOST"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var parameters webhook.WebhookServerParameters
|
||||
|
||||
glog.Info("Starting webhook")
|
||||
// get command line parameters
|
||||
flag.IntVar(¶meters.Port, "port", 8443, "Webhook server port.")
|
||||
flag.StringVar(¶meters.CertFile, "tlsCertFile", "/etc/webhook/certs/cert.pem", "File containing the x509 Certificate for HTTPS.")
|
||||
flag.StringVar(¶meters.KeyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem", "File containing the x509 private key to --tlsCertFile.")
|
||||
flag.Parse()
|
||||
|
||||
pair, err := tls.LoadX509KeyPair(parameters.CertFile, parameters.KeyFile)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to load key pair: %v", err)
|
||||
}
|
||||
|
||||
connectHost, present := os.LookupEnv(connectHostEnv)
|
||||
if !present {
|
||||
glog.Error("")
|
||||
}
|
||||
|
||||
connectTokenName, present := os.LookupEnv(connectTokenSecretNameEnv)
|
||||
if !present {
|
||||
glog.Error("")
|
||||
}
|
||||
|
||||
connectTokenKey, present := os.LookupEnv(connectTokenSecretKeyEnv)
|
||||
if !present {
|
||||
glog.Error("")
|
||||
}
|
||||
|
||||
webhookConfig := webhook.Config{
|
||||
ConnectHost: connectHost,
|
||||
ConnectTokenName: connectTokenName,
|
||||
ConnectTokenKey: connectTokenKey,
|
||||
}
|
||||
webhookServer := &webhook.WebhookServer{
|
||||
Config: webhookConfig,
|
||||
Server: &http.Server{
|
||||
Addr: fmt.Sprintf(":%v", parameters.Port),
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{pair}},
|
||||
},
|
||||
}
|
||||
|
||||
// define http server and server handler
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/inject", webhookServer.Serve)
|
||||
webhookServer.Server.Handler = mux
|
||||
|
||||
// start webhook server in new rountine
|
||||
go func() {
|
||||
if err := webhookServer.Server.ListenAndServeTLS("", ""); err != nil {
|
||||
glog.Errorf("Failed to listen and serve webhook server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// listening OS shutdown singal
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-signalChan
|
||||
|
||||
glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...")
|
||||
webhookServer.Server.Shutdown(context.Background())
|
||||
}
|
42
secret-injector/deploy/deployment.yaml
Normal file
42
secret-injector/deploy/deployment.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: op-secret-injector-webhook-deployment
|
||||
namespace: op-secret-injector
|
||||
labels:
|
||||
app: op-secret-injector
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: op-secret-injector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: op-secret-injector
|
||||
spec:
|
||||
containers:
|
||||
- name: op-secret-injector
|
||||
image: local/onepassword-secrets-injector:v1.1.0
|
||||
imagePullPolicy: Never
|
||||
args:
|
||||
- -tlsCertFile=/etc/webhook/certs/cert.pem
|
||||
- -tlsKeyFile=/etc/webhook/certs/key.pem
|
||||
- -alsologtostderr
|
||||
- -v=4
|
||||
- 2>&1
|
||||
env:
|
||||
- name: OP_CONNECT_HOST
|
||||
value: http://onepassword-connect:8080/
|
||||
- name: OP_CONNECT_TOKEN_NAME
|
||||
value: onepassword-token
|
||||
- name: OP_CONNECT_TOKEN_KEY
|
||||
value: token
|
||||
volumeMounts:
|
||||
- name: webhook-certs
|
||||
mountPath: /etc/webhook/certs
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: webhook-certs
|
||||
secret:
|
||||
secretName: op-secret-injector-webhook-certs
|
22
secret-injector/deploy/mutatingwebhook-ca-bundle.yaml
Normal file
22
secret-injector/deploy/mutatingwebhook-ca-bundle.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
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: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCakNDQWU2Z0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwdGFXNXAKYTNWaVpVTkJNQjRYRFRJd01Ea3lOekl3TkRjMU9Wb1hEVE13TURreU5qSXdORGMxT1Zvd0ZURVRNQkVHQTFVRQpBeE1LYldsdWFXdDFZbVZEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTmlICjZzZVJsZG9CWlRpRVJMeVhwbXFCU3ZOcmJyMWFMcVhpZVVWcXdCcytOUUora1hsazBIRWFldnJRU2QvNnVqY2UKSHpuNFR6Smh3Qk9pYU5BSDN6QUZkeXZxRGwwZVFzNm50R2pDbVFFK0xrUU5PQlVXYmk3WEc2am1tdDA5aFFUVwpTOXg2UDdpai9lUUtLRUJFQTFlRWYvTFZibDZPMVBqa0lXV2E0SjFRMEZoQUtnSjdxUmVJaEg1dkRoVHF3TXVzClZLTEF2bU9xRk03aDNmZ1UzWVltZldpMUFoVnF0VklMYmhkOS8xbzFYM2ZESitFK0dESGMyb0NKK1QvQkxJTmsKOWhTWEhWOTdONFhib1BUWktzZXJFa3JQTnlFYkY4alpvWndBc3FuRVhBNW5sem5vTlJnTFNqSEE0NFZXOGZyawo1RWtJdFNPdkFMMHM1K0FDMnFzQ0F3RUFBYU5oTUY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXCk1CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVcKQkJTSG5DcFRMSDRQeDRkai9oWTVNWEx6TndNeWhqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFvamR6ZzlySgpZTjlJSTJjaUNRS0djZEZGbWtHcGxiandRczBVMVdKY0plZWs3WDh3WWdPMnI3UFhLRklDTEM3aGM5bkUxYnluCkxha2YwMzhUNzRBQzlQOHZXUUJSb2lFMlBKV1BGMjhGTFJWeWgwTWdYQ3dZU20zeitIRDR5TjViWFpNSmJ4WlYKamRza0IzeVpHSW9jZ2RBSk1rU0ptQTN6RkowaHpsY09EZTNOTVA0Ujl4Z0VWczU4bHV5bjl6bm5sL2FDODlHdwpuVnVPRkk0S0dwOFF5NXFjQUxKZndiRGNrNzBjbnRQUEhBN2trT1JtUG41Z2hNSFJPZGxsamxmdXYxVE5RcVo2CjhjUENRRW1zc1ZyajFrVEh6Y3FOUXpqOWVMK2VPMGtyRWw2dzZMcm5YY0dleUxIZVc1cHF6YUY2bWZrTitEZEQKSENjV0U2V1pvTUp2UFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
apiGroups: [""]
|
||||
apiVersions: ["v1"]
|
||||
resources: ["pods"]
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
op-secret-injection: enabled
|
22
secret-injector/deploy/mutatingwebhook.yaml
Normal file
22
secret-injector/deploy/mutatingwebhook.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
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}
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
apiGroups: [""]
|
||||
apiVersions: ["v1"]
|
||||
resources: ["pods"]
|
||||
namespaceSelector:
|
||||
matchLabels:
|
||||
op-secret-injection: enabled
|
13
secret-injector/deploy/service.yaml
Normal file
13
secret-injector/deploy/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: op-secret-injector-webhook-svc
|
||||
namespace: op-secret-injector
|
||||
labels:
|
||||
app: op-secret-injector
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 8443
|
||||
selector:
|
||||
app: op-secret-injector
|
131
secret-injector/deploy/webhook-create-signed-cert.sh
Executable file
131
secret-injector/deploy/webhook-create-signed-cert.sh
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Generate certificate suitable for use with an op-secret-injector webhook service.
|
||||
|
||||
This script uses k8s' CertificateSigningRequest API to a generate a
|
||||
certificate signed by k8s CA suitable for use with op-secret-injector webhook
|
||||
services. This requires permissions to create and approve CSR. See
|
||||
https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster for
|
||||
detailed explanation and additional instructions.
|
||||
|
||||
The server key/cert k8s CA cert are stored in a k8s secret.
|
||||
|
||||
usage: ${0} [OPTIONS]
|
||||
|
||||
The following flags are required.
|
||||
|
||||
--service Service name of webhook.
|
||||
--namespace Namespace where webhook service and secret reside.
|
||||
--secret Secret name for CA certificate and server certificate/key pair.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case ${1} in
|
||||
--service)
|
||||
service="$2"
|
||||
shift
|
||||
;;
|
||||
--secret)
|
||||
secret="$2"
|
||||
shift
|
||||
;;
|
||||
--namespace)
|
||||
namespace="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[ -z "${service}" ] && service=op-secret-injector-webhook-svc
|
||||
[ -z "${secret}" ] && secret=op-secret-injector-webhook-certs
|
||||
[ -z "${namespace}" ] && namespace=default
|
||||
|
||||
if [ ! -x "$(command -v openssl)" ]; then
|
||||
echo "openssl not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
csrName=${service}.${namespace}
|
||||
tmpdir=$(mktemp -d)
|
||||
echo "creating certs in tmpdir ${tmpdir} "
|
||||
|
||||
cat <<EOF >> "${tmpdir}"/csr.conf
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = ${service}
|
||||
DNS.2 = ${service}.${namespace}
|
||||
DNS.3 = ${service}.${namespace}.svc
|
||||
EOF
|
||||
|
||||
openssl genrsa -out "${tmpdir}"/server-key.pem 2048
|
||||
openssl req -new -key "${tmpdir}"/server-key.pem -subj "/CN=${service}.${namespace}.svc" -out "${tmpdir}"/server.csr -config "${tmpdir}"/csr.conf
|
||||
|
||||
# clean-up any previously created CSR for our service. Ignore errors if not present.
|
||||
kubectl delete csr ${csrName} 2>/dev/null || true
|
||||
|
||||
# create server cert/key CSR and send to k8s API
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: certificates.k8s.io/v1beta1
|
||||
kind: CertificateSigningRequest
|
||||
metadata:
|
||||
name: ${csrName}
|
||||
spec:
|
||||
groups:
|
||||
- system:authenticated
|
||||
request: $(< "${tmpdir}"/server.csr base64 | tr -d '\n')
|
||||
usages:
|
||||
- digital signature
|
||||
- key encipherment
|
||||
- server auth
|
||||
EOF
|
||||
|
||||
# verify CSR has been created
|
||||
while true; do
|
||||
if kubectl get csr ${csrName}; then
|
||||
break
|
||||
else
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
|
||||
# approve and fetch the signed certificate
|
||||
kubectl certificate approve ${csrName}
|
||||
# verify certificate has been signed
|
||||
for _ in $(seq 10); do
|
||||
serverCert=$(kubectl get csr ${csrName} -o jsonpath='{.status.certificate}')
|
||||
if [[ ${serverCert} != '' ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ ${serverCert} == '' ]]; then
|
||||
echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${serverCert}" | openssl base64 -d -A -out "${tmpdir}"/server-cert.pem
|
||||
|
||||
|
||||
# create the secret with CA cert and server cert/key
|
||||
kubectl create secret generic ${secret} \
|
||||
--from-file=key.pem="${tmpdir}"/server-key.pem \
|
||||
--from-file=cert.pem="${tmpdir}"/server-cert.pem \
|
||||
--dry-run -o yaml |
|
||||
kubectl -n ${namespace} apply -f -
|
19
secret-injector/deploy/webhook-patch-ca-bundle.sh
Executable file
19
secret-injector/deploy/webhook-patch-ca-bundle.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
CA_BUNDLE=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}')
|
||||
|
||||
if [ -z "${CA_BUNDLE}" ]; then
|
||||
CA_BUNDLE=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.ca\.crt}")
|
||||
fi
|
||||
|
||||
export CA_BUNDLE
|
||||
|
||||
if command -v envsubst >/dev/null 2>&1; then
|
||||
envsubst
|
||||
else
|
||||
sed -e "s|\${CA_BUNDLE}|${CA_BUNDLE}|g"
|
||||
fi
|
214
secret-injector/golangci.yml
Normal file
214
secret-injector/golangci.yml
Normal file
@@ -0,0 +1,214 @@
|
||||
service:
|
||||
# When updating this, also update the version stored in docker/build-tools/Dockerfile in the multicloudlab/tools repo.
|
||||
golangci-lint-version: 1.18.x # use the fixed version to not introduce new linters unexpectedly
|
||||
run:
|
||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||
deadline: 20m
|
||||
|
||||
# which dirs to skip: they won't be analyzed;
|
||||
# can use regexp here: generated.*, regexp is applied on full path;
|
||||
# default value is empty list, but next dirs are always skipped independently
|
||||
# from this option's value:
|
||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||
skip-dirs:
|
||||
- genfiles$
|
||||
- vendor$
|
||||
|
||||
# which files to skip: they will be analyzed, but issues from them
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
- ".*\\.gen\\.go"
|
||||
|
||||
linters:
|
||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- misspell
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
# don't enable:
|
||||
# - gocritic
|
||||
# - bodyclose
|
||||
# - depguard
|
||||
# - dogsled
|
||||
# - dupl
|
||||
# - funlen
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits
|
||||
# - gocognit
|
||||
# - godox
|
||||
# - maligned
|
||||
# - nakedret
|
||||
# - prealloc
|
||||
# - scopelint
|
||||
# - whitespace
|
||||
# - stylecheck
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: false
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: false
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
golint:
|
||||
# minimal confidence for issues, default is 0.8
|
||||
min-confidence: 0.0
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
goimports:
|
||||
# put imports beginning with prefix after 3rd-party packages;
|
||||
# it's a comma-separated list of prefixes
|
||||
local-prefixes: github.com/IBM/
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
misspell:
|
||||
# Correct spellings using locale preferences for US or UK.
|
||||
# Default is to use a neutral variety of English.
|
||||
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||
locale: US
|
||||
ignore-words:
|
||||
- cancelled
|
||||
lll:
|
||||
# max line length, lines longer will be reported. Default is 120.
|
||||
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
|
||||
line-length: 300
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
unparam:
|
||||
# call graph construction algorithm (cha, rta). In general, use cha for libraries,
|
||||
# and rta for programs with main packages. Default is cha.
|
||||
algo: cha
|
||||
|
||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
gocritic:
|
||||
enabled-checks:
|
||||
- appendCombine
|
||||
- argOrder
|
||||
- assignOp
|
||||
- badCond
|
||||
- boolExprSimplify
|
||||
- builtinShadow
|
||||
- captLocal
|
||||
- caseOrder
|
||||
- codegenComment
|
||||
- commentedOutCode
|
||||
- commentedOutImport
|
||||
- defaultCaseOrder
|
||||
- deprecatedComment
|
||||
- docStub
|
||||
- dupArg
|
||||
- dupBranchBody
|
||||
- dupCase
|
||||
- dupSubExpr
|
||||
- elseif
|
||||
- emptyFallthrough
|
||||
- equalFold
|
||||
- flagDeref
|
||||
- flagName
|
||||
- hexLiteral
|
||||
- indexAlloc
|
||||
- initClause
|
||||
- methodExprCall
|
||||
- nilValReturn
|
||||
- octalLiteral
|
||||
- offBy1
|
||||
- rangeExprCopy
|
||||
- regexpMust
|
||||
- sloppyLen
|
||||
- stringXbytes
|
||||
- switchTrue
|
||||
- typeAssertChain
|
||||
- typeSwitchVar
|
||||
- typeUnparen
|
||||
- underef
|
||||
- unlambda
|
||||
- unnecessaryBlock
|
||||
- unslice
|
||||
- valSwap
|
||||
- weakCond
|
||||
|
||||
# Unused
|
||||
# - yodaStyleExpr
|
||||
# - appendAssign
|
||||
# - commentFormatting
|
||||
# - emptyStringTest
|
||||
# - exitAfterDefer
|
||||
# - ifElseChain
|
||||
# - hugeParam
|
||||
# - importShadow
|
||||
# - nestingReduce
|
||||
# - paramTypeCombine
|
||||
# - ptrToRefParam
|
||||
# - rangeValCopy
|
||||
# - singleCaseSwitch
|
||||
# - sloppyReassign
|
||||
# - unlabelStmt
|
||||
# - unnamedResult
|
||||
# - wrapperFunc
|
||||
|
||||
issues:
|
||||
# List of regexps of issue texts to exclude, empty list by default.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`
|
||||
exclude:
|
||||
- composite literal uses unkeyed fields
|
||||
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on test files.
|
||||
- path: _test\.go$|^tests/|^samples/
|
||||
linters:
|
||||
- errcheck
|
||||
- maligned
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: true
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
||||
|
469
secret-injector/pkg/webhook/webhook.go
Normal file
469
secret-injector/pkg/webhook/webhook.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
)
|
||||
|
||||
const (
|
||||
// binVolumeName is the name of the volume where the OP CLI binary is stored.
|
||||
binVolumeName = "op-bin"
|
||||
|
||||
// binVolumeMountPath is the mount path where the OP CLI binary can be found.
|
||||
binVolumeMountPath = "/op/bin/"
|
||||
)
|
||||
|
||||
// binVolume is the shared, in-memory volume where the OP CLI binary lives.
|
||||
var binVolume = corev1.Volume{
|
||||
Name: binVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
EmptyDir: &corev1.EmptyDirVolumeSource{
|
||||
Medium: corev1.StorageMediumMemory,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// binVolumeMount is the shared volume mount where the OP CLI binary lives.
|
||||
var binVolumeMount = corev1.VolumeMount{
|
||||
Name: binVolumeName,
|
||||
MountPath: binVolumeMountPath,
|
||||
ReadOnly: true,
|
||||
}
|
||||
|
||||
var (
|
||||
runtimeScheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(runtimeScheme)
|
||||
deserializer = codecs.UniversalDeserializer()
|
||||
|
||||
// (https://github.com/kubernetes/kubernetes/issues/57982)
|
||||
defaulter = runtime.ObjectDefaulter(runtimeScheme)
|
||||
)
|
||||
|
||||
var ignoredNamespaces = []string{
|
||||
metav1.NamespaceSystem,
|
||||
metav1.NamespacePublic,
|
||||
}
|
||||
|
||||
const (
|
||||
injectionStatus = "operator.1password.io/status"
|
||||
injectAnnotation = "operator.1password.io/inject"
|
||||
versionAnnotation = "operator.1password.io/version"
|
||||
)
|
||||
|
||||
type WebhookServer struct {
|
||||
Config Config
|
||||
Server *http.Server
|
||||
}
|
||||
|
||||
// Webhook Server parameters
|
||||
type WebhookServerParameters struct {
|
||||
Port int // webhook server port
|
||||
CertFile string // path to the x509 certificate for https
|
||||
KeyFile string // path to the x509 private key matching `CertFile`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ConnectHost string
|
||||
ConnectTokenName string
|
||||
ConnectTokenKey string
|
||||
}
|
||||
|
||||
type patchOperation struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = corev1.AddToScheme(runtimeScheme)
|
||||
_ = admissionregistrationv1beta1.AddToScheme(runtimeScheme)
|
||||
_ = v1.AddToScheme(runtimeScheme)
|
||||
}
|
||||
|
||||
func applyDefaultsWorkaround(containers []corev1.Container, volumes []corev1.Volume) {
|
||||
defaulter.Default(&corev1.Pod{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: containers,
|
||||
Volumes: volumes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check whether the target resoured need to be mutated
|
||||
func mutationRequired(ignoredList []string, metadata *metav1.ObjectMeta) bool {
|
||||
// skip special kubernete system namespaces
|
||||
for _, namespace := range ignoredList {
|
||||
if metadata.Namespace == namespace {
|
||||
glog.Infof("Skip mutation for %v for it's in special namespace:%v", metadata.Name, metadata.Namespace)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
annotations := metadata.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = map[string]string{}
|
||||
}
|
||||
|
||||
status := annotations[injectionStatus]
|
||||
_, enabled := annotations[injectAnnotation]
|
||||
|
||||
// determine whether to perform mutation based on annotation for the target resource
|
||||
required := false
|
||||
if strings.ToLower(status) != "injected" && enabled {
|
||||
required = true
|
||||
}
|
||||
|
||||
glog.Infof("Mutation policy for %v/%v: status: %q required:%v", metadata.Namespace, metadata.Name, status, required)
|
||||
return required
|
||||
}
|
||||
|
||||
func addContainers(target, added []corev1.Container, basePath string) (patch []patchOperation) {
|
||||
first := len(target) == 0
|
||||
var value interface{}
|
||||
for _, add := range added {
|
||||
value = add
|
||||
path := basePath
|
||||
if first {
|
||||
first = false
|
||||
value = []corev1.Container{add}
|
||||
} else {
|
||||
path = path + "/-"
|
||||
}
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "add",
|
||||
Path: path,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
func addVolume(target, added []corev1.Volume, basePath string) (patch []patchOperation) {
|
||||
first := len(target) == 0
|
||||
var value interface{}
|
||||
for _, add := range added {
|
||||
value = add
|
||||
path := basePath
|
||||
if first {
|
||||
first = false
|
||||
value = []corev1.Volume{add}
|
||||
} else {
|
||||
path = path + "/-"
|
||||
}
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "add",
|
||||
Path: path,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
func updateAnnotation(target map[string]string, added map[string]string) (patch []patchOperation) {
|
||||
for key, value := range added {
|
||||
if target == nil || target[key] == "" {
|
||||
target = map[string]string{}
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "add",
|
||||
Path: "/metadata/annotations",
|
||||
Value: map[string]string{
|
||||
key: value,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "replace",
|
||||
Path: "/metadata/annotations/" + key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
// main mutation process
|
||||
func (whsvr *WebhookServer) mutate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
|
||||
ctx := context.Background()
|
||||
req := ar.Request
|
||||
var pod corev1.Pod
|
||||
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
|
||||
glog.Errorf("Could not unmarshal raw object: %v", err)
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v (%v) UID=%v patchOperation=%v UserInfo=%v",
|
||||
req.Kind, req.Namespace, req.Name, pod.Name, req.UID, req.Operation, req.UserInfo)
|
||||
|
||||
// determine whether to perform mutation
|
||||
if !mutationRequired(ignoredNamespaces, &pod.ObjectMeta) {
|
||||
glog.Infof("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name)
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
containersStr := pod.Annotations[injectAnnotation]
|
||||
|
||||
containers := map[string]struct{}{}
|
||||
|
||||
for _, container := range strings.Split(containersStr, ",") {
|
||||
containers[container] = struct{}{}
|
||||
}
|
||||
|
||||
version, ok := pod.Annotations[versionAnnotation]
|
||||
if !ok {
|
||||
version = "latest"
|
||||
}
|
||||
|
||||
mutated := false
|
||||
|
||||
var patch []patchOperation
|
||||
for i, c := range pod.Spec.InitContainers {
|
||||
_, mutate := containers[c.Name]
|
||||
if !mutate {
|
||||
continue
|
||||
}
|
||||
c, didMutate, initContainerPatch, err := whsvr.mutateContainer(ctx, &c, i)
|
||||
if err != nil {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if didMutate {
|
||||
mutated = true
|
||||
pod.Spec.InitContainers[i] = *c
|
||||
}
|
||||
patch = append(patch, initContainerPatch...)
|
||||
}
|
||||
|
||||
for i, c := range pod.Spec.Containers {
|
||||
_, mutate := containers[c.Name]
|
||||
if !mutate {
|
||||
continue
|
||||
}
|
||||
|
||||
c, didMutate, containerPatch, err := whsvr.mutateContainer(ctx, &c, i)
|
||||
if err != nil {
|
||||
glog.Errorf("Error occured mutating container: ", err)
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
patch = append(patch, containerPatch...)
|
||||
if didMutate {
|
||||
mutated = true
|
||||
pod.Spec.Containers[i] = *c
|
||||
}
|
||||
}
|
||||
|
||||
// binInitContainer is the container that pulls the OP CLI
|
||||
// into a shared volume mount.
|
||||
var binInitContainer = corev1.Container{
|
||||
Name: "copy-op-bin",
|
||||
Image: "op-example" + ":" + version,
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
Command: []string{"sh", "-c",
|
||||
fmt.Sprintf("cp /usr/local/bin/op %s", binVolumeMountPath)},
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: binVolumeName,
|
||||
MountPath: binVolumeMountPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !mutated {
|
||||
glog.Infof("No mutations made for %s/%s", pod.Namespace, pod.Name)
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
annotations := map[string]string{injectionStatus: "injected"}
|
||||
patchBytes, err := createOPCLIPatch(&pod, annotations, []corev1.Container{binInitContainer}, patch)
|
||||
if err != nil {
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("AdmissionResponse: patch=%v\n", string(patchBytes))
|
||||
return &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
Patch: patchBytes,
|
||||
PatchType: func() *v1beta1.PatchType {
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
return &pt
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
||||
// create mutation patch for resoures
|
||||
func createOPCLIPatch(pod *corev1.Pod, annotations map[string]string, containers []corev1.Container, patch []patchOperation) ([]byte, error) {
|
||||
|
||||
patch = append(patch, addVolume(pod.Spec.Volumes, []corev1.Volume{binVolume}, "/spec/volumes")...)
|
||||
patch = append(patch, addContainers(pod.Spec.InitContainers, containers, "/spec/initContainers")...)
|
||||
patch = append(patch, updateAnnotation(pod.Annotations, annotations)...)
|
||||
|
||||
return json.Marshal(patch)
|
||||
}
|
||||
|
||||
func createOPConnectPatch(container *corev1.Container, containerIndex int, host, tokenSecretName, tokenSecretKey string) []patchOperation {
|
||||
var patch []patchOperation
|
||||
connectHostEnvVar := corev1.EnvVar{
|
||||
Name: "OP_CONNECT_HOST",
|
||||
Value: host,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
patch = append(patch, setEnvironment(*container, containerIndex, envs, "/spec/containers")...)
|
||||
|
||||
return patch
|
||||
}
|
||||
|
||||
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
|
||||
if len(container.Command) == 0 {
|
||||
return container, false, nil, fmt.Errorf("not attaching OP to the container %s: the podspec does not define a command", container.Name)
|
||||
}
|
||||
|
||||
// Prepend the command with op run --
|
||||
container.Command = append([]string{binVolumeMountPath + "op", "run", "--"}, container.Command...)
|
||||
|
||||
var patch []patchOperation
|
||||
|
||||
// adding the cli to the container using a volume mount
|
||||
path := fmt.Sprintf("%s/%d/volumeMounts", "/spec/containers", containerIndex)
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "add",
|
||||
Path: path,
|
||||
Value: []corev1.VolumeMount{binVolumeMount},
|
||||
})
|
||||
|
||||
// replacing the container command with a command prepended with op run
|
||||
path = fmt.Sprintf("%s/%d/command", "/spec/containers", containerIndex)
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "replace",
|
||||
Path: path,
|
||||
Value: container.Command,
|
||||
})
|
||||
|
||||
//creating patch for adding conenct environment variables to container
|
||||
patch = append(patch, createOPConnectPatch(container, containerIndex, whsvr.Config.ConnectHost, whsvr.Config.ConnectTokenName, whsvr.Config.ConnectTokenKey)...)
|
||||
return container, true, patch, nil
|
||||
}
|
||||
|
||||
func setEnvironment(container corev1.Container, containerIndex int, addedEnv []corev1.EnvVar, basePath string) (patch []patchOperation) {
|
||||
first := len(container.Env) == 0
|
||||
var value interface{}
|
||||
for _, add := range addedEnv {
|
||||
path := fmt.Sprintf("%s/%d/env", basePath, containerIndex)
|
||||
value = add
|
||||
if first {
|
||||
first = false
|
||||
value = []corev1.EnvVar{add}
|
||||
} else {
|
||||
path = path + "/-"
|
||||
}
|
||||
patch = append(patch, patchOperation{
|
||||
Op: "add",
|
||||
Path: path,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
return patch
|
||||
}
|
||||
|
||||
// Serve method for webhook server
|
||||
func (whsvr *WebhookServer) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
if data, err := ioutil.ReadAll(r.Body); err == nil {
|
||||
body = data
|
||||
}
|
||||
}
|
||||
if len(body) == 0 {
|
||||
glog.Error("empty body")
|
||||
http.Error(w, "empty body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// verify the content type is accurate
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
glog.Errorf("Content-Type=%s, expect application/json", contentType)
|
||||
http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
var admissionResponse *v1beta1.AdmissionResponse
|
||||
ar := v1beta1.AdmissionReview{}
|
||||
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {
|
||||
glog.Errorf("Can't decode body: %v", err)
|
||||
admissionResponse = &v1beta1.AdmissionResponse{
|
||||
Result: &metav1.Status{
|
||||
Message: err.Error(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
admissionResponse = whsvr.mutate(&ar)
|
||||
}
|
||||
|
||||
admissionReview := v1beta1.AdmissionReview{}
|
||||
if admissionResponse != nil {
|
||||
admissionReview.Response = admissionResponse
|
||||
if ar.Request != nil {
|
||||
admissionReview.Response.UID = ar.Request.UID
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := json.Marshal(admissionReview)
|
||||
if err != nil {
|
||||
glog.Errorf("Can't encode response: %v", err)
|
||||
http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
glog.Infof("Ready to write reponse ...")
|
||||
if _, err := w.Write(resp); err != nil {
|
||||
glog.Errorf("Can't write response: %v", err)
|
||||
http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
9
secret-injector/test/Dockerfile
Normal file
9
secret-injector/test/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM ubuntu:latest
|
||||
ARG VERSION
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip jq && \
|
||||
curl -o 1password.zip https://bucket.agilebits.com/cli-private-beta/v2/op_linux_amd64_v2-alpha2.zip && \
|
||||
unzip 1password.zip -d /usr/local/bin && \
|
||||
rm 1password.zip
|
||||
|
||||
CMD ["op"]
|
Reference in New Issue
Block a user