Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support vault cacert bytes env #507

Merged
merged 13 commits into from
Oct 24, 2023
36 changes: 26 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ PKG=github.com/hashicorp/vault-k8s/version
LDFLAGS?="-X '$(PKG).Version=v$(VERSION)'"
TESTARGS ?= '-test.v'

HELM_CHART_VERSION ?= 0.25.0
VAULT_TLS?=false
VAULT_HELM_CHART_VERSION ?= 0.25.0
VAULT_HELM_FLAGS?=--repo https://helm.releases.hashicorp.com --version=$(VAULT_HELM_CHART_VERSION) \
--wait --timeout=5m \
--values=test/vault/dev.values.yaml \
--set 'injector.image.tag=$(VERSION)'

.PHONY: all test build image clean version deploy exercise teardown
ifeq ($(VAULT_TLS), true)
VAULT_HELM_FLAGS += --values=test/vault/vault-tls-dev.values.yaml \
--set "injector.extraEnvironmentVars.AGENT_INJECT_VAULT_CACERT_BYTES=$(shell kubectl get secret vault-cert -o=jsonpath="{.data.ca\.crt}")"
endif

.PHONY: all test build image clean version deploy deploy-tls exercise teardown install-cert-manager
all: build

version:
Expand All @@ -35,14 +45,12 @@ image: build
# Run multiple times to deploy new builds of the injector.
deploy: image
kind load docker-image hashicorp/vault-k8s:$(VERSION)
helm upgrade --install vault vault --repo https://helm.releases.hashicorp.com --version=$(HELM_CHART_VERSION) \
--wait --timeout=5m \
--set 'server.dev.enabled=true' \
--set 'server.logLevel=debug' \
--set 'injector.image.tag=$(VERSION)' \
--set 'injector.image.pullPolicy=Never' \
--set 'injector.affinity=null' \
--set 'injector.annotations.deployed=unix-$(shell date +%s)'
helm upgrade --install vault vault $(VAULT_HELM_FLAGS)
kubectl delete pod -l "app.kubernetes.io/instance=vault"
kubectl wait --for=condition=Ready --timeout=5m pod -l "app.kubernetes.io/instance=vault"

deploy-tls: install-cert-manager
VAULT_TLS=true make deploy

# Populates the Vault dev server with a secret, configures kubernetes auth, and
# deploys an nginx pod with annotations to have the secret injected.
Expand All @@ -66,9 +74,17 @@ exercise:
kubectl wait --for=condition=Ready --timeout=5m pod nginx
kubectl exec nginx -c nginx -- cat /vault/secrets/secret.txt

install-cert-manager:
helm upgrade --install cert-manager cert-manager --repo https://charts.jetstack.io \
--set installCRDs=true \
--wait=true --timeout=5m
kubectl apply -f 'test/cert-manager/*'
kubectl wait --for=condition=Ready --timeout=5m certificate vault-certificate

# Teardown any resources created in deploy and exercise targets.
teardown:
helm uninstall vault || true
helm uninstall cert-manager || true
kubectl delete --ignore-not-found serviceaccount test-app-sa
kubectl delete --ignore-not-found pod nginx

Expand Down
7 changes: 6 additions & 1 deletion agent-inject/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,14 @@ type Vault struct {
AuthConfig map[string]interface{}

// CACert is the name of the Certificate Authority certificate
// to use when validating Vault's server certificates.
// to use when validating Vault's server certificates. It takes
// precedence over CACertBytes.
CACert string

// CACertBytes is the contents of the CA certificate to trust
// for TLS with Vault as a PEM-encoded certificate or bundle.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this expected to be b64 encoded? Should we specify one way or another?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be PEM-encoded or base64. I added some comments in d50e787 to document the optional base64 encoding.

CACertBytes string

// CAKey is the name of the Certificate Authority key
// to use when validating Vault's server certificates.
CAKey string
Expand Down
24 changes: 24 additions & 0 deletions agent-inject/agent/container_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package agent

import (
"encoding/base64"
"path"
"strconv"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -139,6 +140,20 @@ func (a *Agent) ContainerEnvVars(init bool) ([]corev1.EnvVar, error) {
}
}

if a.Vault.CACertBytes != "" {
envs = append(envs, corev1.EnvVar{
Name: "VAULT_CACERT_BYTES",
Value: decodeIfBase64(a.Vault.CACertBytes),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess going back to my previous comment, is there any reason not to make b64 encoding a requirement? The formatting of certs with all their carriage returns is to me frustrating at best trying to get into string values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the benefit of making b64 required. Supporting the raw PEM is nice because it's how the environment variable is eventually consumed anyway, and supporting b64 is nice because as you say it can be frustrating when there are multiple levels of interpretation in the automation that gets the value into place.

})
// TODO(tomhjp): Remove when consul-template supports VAULT_CACERT_BYTES
if a.Vault.CACert == "" {
envs = append(envs, corev1.EnvVar{
Name: "VAULT_CACERT",
Value: path.Join(tokenVolumePath, caFileName),
})
}
}

// Add IRSA AWS Env variables for vault containers
if a.Vault.AuthType == "aws" {
envMap := a.getAwsEnvsFromContainer(a.Pod)
Expand All @@ -160,3 +175,12 @@ func (a *Agent) ContainerEnvVars(init bool) ([]corev1.EnvVar, error) {

return envs, nil
}

func decodeIfBase64(s string) string {
decoded, err := base64.StdEncoding.DecodeString(s)
if err == nil {
return string(decoded)
}

return s
}
8 changes: 7 additions & 1 deletion agent-inject/agent/container_init_sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"encoding/json"
"fmt"

"github.com/evanphx/json-patch"
jsonpatch "github.com/evanphx/json-patch"
corev1 "k8s.io/api/core/v1"
)

Expand Down Expand Up @@ -67,6 +67,12 @@ func (a *Agent) ContainerInitSidecar() (corev1.Container, error) {
MountPath: tlsSecretVolumePath,
ReadOnly: true,
})
} else if a.Vault.CACert == "" && a.Vault.CACertBytes != "" {
// TODO(tomhjp): Remove when consul-template supports VAULT_CACERT_BYTES.
// consul-template does not yet support VAULT_CACERT_BYTES, so we write
// it out to a file and set VAULT_CACERT as well to ensure templating
// picks up the CA.
arg = prependWriteCAToFile(arg)
}

if a.VaultAgentCache.Persist {
Expand Down
10 changes: 10 additions & 0 deletions agent-inject/agent/container_sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func (a *Agent) ContainerSidecar() (corev1.Container, error) {
MountPath: tlsSecretVolumePath,
ReadOnly: true,
})
} else if a.Vault.CACert == "" && a.Vault.CACertBytes != "" {
// TODO(tomhjp): Remove when consul-template supports VAULT_CACERT_BYTES.
// consul-template does not yet support VAULT_CACERT_BYTES, so we write
// it out to a file and set VAULT_CACERT as well to ensure templating
// picks up the CA.
arg = prependWriteCAToFile(arg)
}

if a.VaultAgentCache.Persist {
Expand Down Expand Up @@ -136,6 +142,10 @@ func (a *Agent) ContainerSidecar() (corev1.Container, error) {
return newContainer, nil
}

func prependWriteCAToFile(arg string) string {
return fmt.Sprintf(`printf "%%s" "${VAULT_CACERT_BYTES}" > %s/%s && %s`, tokenVolumePath, caFileName, arg)
}

// Valid resource notations: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu
func (a *Agent) parseResources() (corev1.ResourceRequirements, error) {
resources := corev1.ResourceRequirements{}
Expand Down
1 change: 1 addition & 0 deletions agent-inject/agent/container_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
tokenVolumeNameInit = "home-init"
tokenVolumeNameSidecar = "home-sidecar"
tokenVolumePath = "/home/vault"
caFileName = "ca.crt"
configVolumeName = "vault-config"
configVolumePath = "/vault/configs"
secretVolumeName = "vault-secrets"
Expand Down
2 changes: 2 additions & 0 deletions agent-inject/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Handler struct {
// If this is false, injection is default.
RequireAnnotation bool
VaultAddress string
VaultCACertBytes string
VaultAuthType string
VaultAuthPath string
VaultNamespace string
Expand Down Expand Up @@ -226,6 +227,7 @@ func (h *Handler) Mutate(req *admissionv1.AdmissionRequest) *admissionv1.Admissi
err := fmt.Errorf("error creating new agent sidecar: %s", err)
return admissionError(req.UID, err)
}
agentSidecar.Vault.CACertBytes = h.VaultCACertBytes

h.Log.Debug("validating agent configuration..")
err = agentSidecar.Validate()
Expand Down
2 changes: 2 additions & 0 deletions subcommand/injector/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Command struct {
flagAutoName string // MutatingWebhookConfiguration for updating
flagAutoHosts string // SANs for the auto-generated TLS cert.
flagVaultService string // Name of the Vault service
flagVaultCACertBytes string // CA Cert to trust for TLS with Vault.
flagProxyAddress string // HTTP proxy address used to talk to the Vault service
flagVaultImage string // Name of the Vault Image to use
flagVaultAuthType string // Type of Vault Auth Method to use
Expand Down Expand Up @@ -194,6 +195,7 @@ func (c *Command) Run(args []string) int {
// Build the HTTP handler and server
injector := agentInject.Handler{
VaultAddress: c.flagVaultService,
VaultCACertBytes: c.flagVaultCACertBytes,
VaultAuthType: c.flagVaultAuthType,
VaultAuthPath: c.flagVaultAuthPath,
VaultNamespace: c.flagVaultNamespace,
Expand Down
9 changes: 9 additions & 0 deletions subcommand/injector/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ type Specification struct {
// VaultAddr is the AGENT_INJECT_VAULT_ADDR environment variable.
VaultAddr string `split_words:"true"`

// VaultCACertBytes is the AGENT_INJECT_VAULT_CACERT_BYTES environment variable.
// Specifies the CA cert to trust for TLS with Vault.
VaultCACertBytes string `envconfig:"AGENT_INJECT_VAULT_CACERT_BYTES"`

// ProxyAddr is the AGENT_INJECT_PROXY_ADDR environment variable.
ProxyAddr string `split_words:"true"`

Expand Down Expand Up @@ -159,6 +163,8 @@ func (c *Command) init() {
fmt.Sprintf("Docker image for Vault. Defaults to %q.", agent.DefaultVaultImage))
c.flagSet.StringVar(&c.flagVaultService, "vault-address", "",
"Address of the Vault server.")
c.flagSet.StringVar(&c.flagVaultCACertBytes, "vault-cacert-bytes", "",
"CA certificate to trust for TLS with Vault, specified as a PEM-encoded certificate or bundle.")
c.flagSet.StringVar(&c.flagProxyAddress, "proxy-address", "",
"HTTP proxy address used to talk to the Vault service.")
c.flagSet.StringVar(&c.flagVaultAuthType, "vault-auth-type", agent.DefaultVaultAuthType,
Expand Down Expand Up @@ -295,6 +301,9 @@ func (c *Command) parseEnvs() error {
if envs.VaultAddr != "" {
c.flagVaultService = envs.VaultAddr
}
if envs.VaultCACertBytes != "" {
c.flagVaultCACertBytes = envs.VaultCACertBytes
}

if envs.ProxyAddr != "" {
c.flagProxyAddress = envs.ProxyAddr
Expand Down
1 change: 1 addition & 0 deletions subcommand/injector/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func TestCommandEnvs(t *testing.T) {
}{
{env: "AGENT_INJECT_LISTEN", value: ":8080", cmdPtr: &cmd.flagListen},
{env: "AGENT_INJECT_VAULT_ADDR", value: "http://vault:8200", cmdPtr: &cmd.flagVaultService},
{env: "AGENT_INJECT_VAULT_CACERT_BYTES", value: "foo", cmdPtr: &cmd.flagVaultCACertBytes},
{env: "AGENT_INJECT_PROXY_ADDR", value: "http://proxy:3128", cmdPtr: &cmd.flagProxyAddress},
{env: "AGENT_INJECT_VAULT_AUTH_PATH", value: "auth-path-test", cmdPtr: &cmd.flagVaultAuthPath},
{env: "AGENT_INJECT_VAULT_IMAGE", value: "hashicorp/vault:1.13.3", cmdPtr: &cmd.flagVaultImage},
Expand Down
31 changes: 31 additions & 0 deletions test/cert-manager/ca.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: vault-ca
spec:
isCA: true
commonName: Vault CA
secretName: vault-ca
duration: 87660h # 10 years
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned
kind: Issuer
group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: vault-ca-issuer
spec:
ca:
secretName: vault-ca
20 changes: 20 additions & 0 deletions test/cert-manager/vault-cert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: vault-certificate
spec:
secretName: vault-cert
duration: 24h
renewBefore: 144m # roughly 10% of 24h
dnsNames:
- vault
- vault.default
- vault.default.svc
- vault-internal
- vault-internal.default
- vault-internal.default.svc
ipAddresses:
- "127.0.0.1"
issuerRef:
name: vault-ca-issuer
commonName: Vault Server
8 changes: 8 additions & 0 deletions test/vault/dev.values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
injector:
image:
pullPolicy: Never
affinity: null
server:
dev:
enabled: true
logLevel: debug
39 changes: 39 additions & 0 deletions test/vault/vault-tls-dev.values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
global:
tlsDisable: false
server:
# Move the default TLS-disabled dev listener out of the way so we can add our
# own listener on 8200 that does use TLS.
extraArgs: "-dev-listen-address=127.0.0.1:8202 -config=/etc/config/listener.hcl"
extraEnvironmentVars:
VAULT_CACERT: /etc/tls/ca.crt
volumeMounts:
- name: cert
mountPath: /etc/tls
readOnly: true
- name: config
mountPath: /etc/config
readOnly: true
volumes:
- name: cert
secret:
secretName: vault-cert
- name: config
emptyDir: {}
extraInitContainers:
- name: write-config
image: "alpine"
command: [sh, -c]
args:
- |
cat <<EOF > /etc/config/listener.hcl
listener "tcp" {
address = "[::]:8200"
tls_cert_file = "/etc/tls/tls.crt"
tls_key_file = "/etc/tls/tls.key"
proxy_protocol_behavior = "allow_authorized"
proxy_protocol_authorized_addrs = "[::]:8200"
}
EOF
volumeMounts:
- name: config
mountPath: /etc/config