Use the Secrets Proxy
The Secrets Proxy lets Crossplane providers read and write secrets directly to HashiCorp Vault instead of storing them as Kubernetes Secrets. Providers use the standard Kubernetes Secret API. The Secrets Proxy intercepts those calls and routes them to Vault transparently.
Prerequisites
Before you begin, ensure you have:
- kubectl installed
- Helm installed
- The Vault CLI installed
- The
upCLI installed - A UXP cluster running version 2.2 or later and a valid license
- A HashiCorp Vault instance reachable from your cluster, with Kubernetes auth enabled
Enable the Secrets Proxy
Enable the Secrets Proxy on your UXP installation:
helm repo add upbound-stable https://charts.upbound.io/stable && helm repo update
helm install crossplane \
--namespace crossplane-system \
--create-namespace \
upbound-stable/crossplane \
--devel \
--set upbound.secretsProxy.enabled=true
kubectl get pods -n crossplane-system -w
Configure Vault
Store the AWS credentials that the provider reads. The Secrets Proxy serves
these to providers as if they were a Kubernetes Secret, using the ini format
expected by the AWS provider family:
vault kv put secret/crossplane-system/aws-official-creds credentials="
[default]
aws_access_key_id = <AWS_ACCESS_KEY_ID>
aws_secret_access_key = <AWS_SECRET_ACCESS_KEY>
"
Create a policy granting read access to secrets in crossplane-system:
vault policy write crossplane-policy - <<'EOF'
path "secret/data/crossplane-system/*" {
capabilities = ["read", "list"]
}
EOF
Configure Vault Kubernetes auth
Configure Vault to trust service account tokens from your UXP cluster so the Secrets Proxy sidecar can authenticate on behalf of provider pods.
-
Create a service account for Vault token review:
kubectl create serviceaccount vault-auth -n crossplane-system
kubectl create clusterrolebinding vault-auth-delegator \
--clusterrole=system:auth-delegator \
--serviceaccount=crossplane-system:vault-auth -
Generate a long-lived token for the reviewer service account:
REVIEWER_JWT=$(kubectl create token vault-auth \
-n crossplane-system --duration=8760h) -
Retrieve the cluster CA certificate and API server URL. For local clusters (Kind, minikube), use the in-cluster Kubernetes service address so Vault can reach the API server from inside the cluster:
KUBE_CA_CERT=$(kubectl config view --raw --minify --flatten \
--output='jsonpath={.clusters[].cluster.certificate-authority-data}' \
| base64 --decode)
KUBE_HOST="https://kubernetes.default.svc.cluster.local" -
Enable the Kubernetes auth method and configure it with the cluster details:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="${KUBE_HOST}" \
kubernetes_ca_cert="${KUBE_CA_CERT}" \
token_reviewer_jwt="${REVIEWER_JWT}" -
Create a role that binds all service accounts in
crossplane-systemto the policy:vault write auth/kubernetes/role/crossplane \
bound_service_account_names="*" \
bound_service_account_namespaces="crossplane-system" \
bound_audiences="https://kubernetes.default.svc.cluster.local" \
policies=crossplane-policy \
ttl=1h
Install the Secret Store add-on
Apply the add-on to deploy the Secrets Proxy backend:
apiVersion: pkg.upbound.io/v1beta1
kind: AddOn
metadata:
name: secret-store-vault
spec:
package: xpkg.upbound.io/upbound/secret-store-vault-addon:v0.1.0
kubectl apply -f addon.yaml
Once the add-on is ready, create a StoreConfig pointing to your Vault
instance:
apiVersion: vault.secrets.upbound.io/v1alpha1
kind: StoreConfig
metadata:
name: vault
spec:
address: http://vault.vault-system.svc.cluster.local:8200
mountPath: secret
kvVersion: KVv2
auth:
method: kubernetes
kubernetes:
role: crossplane
kubectl apply -f storeconfig.yaml
kubectl get storeconfigs
Configure the webhook
Apply the webhook configuration to inject the Secrets Proxy sidecar into Crossplane and provider pods. The webhook restarts all matching pods on apply:
apiVersion: secretsproxy.upbound.io/v1alpha1
kind: WebhookConfig
metadata:
name: crossplane-app
spec:
objectSelector:
matchLabels:
app: crossplane
---
apiVersion: secretsproxy.upbound.io/v1alpha1
kind: WebhookConfig
metadata:
name: crossplane-provider
spec:
objectSelector:
matchExpressions:
- key: pkg.crossplane.io/provider
operator: Exists
kubectl apply -f webhookconfig.yaml
Install providers and functions
-
Install the AWS provider family and IAM provider. Provider pods start with the Secrets Proxy sidecar injected because the webhook matches the
pkg.crossplane.io/providerlabel:apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: upbound-provider-aws-iam
spec:
package: xpkg.upbound.io/upbound/provider-aws-iam:v2.2.0
ignoreCrossplaneConstraints: true
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: upbound-provider-family-aws
spec:
package: xpkg.upbound.io/upbound/provider-family-aws:v2.2.0
ignoreCrossplaneConstraints: truekubectl apply -f providers.yaml -
Install the pipeline functions:
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2
---
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-auto-ready
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.6.0kubectl apply -f functions.yaml
- Wait for providers and functions to become healthy, then create the provider
config. The
aws-official-credssecret lives in Vault. The Secrets Proxy intercepts the Secret API call and serves it transparently:
apiVersion: aws.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
namespace: crossplane-system
spec:
credentials:
secretRef:
key: credentials
name: aws-official-creds
namespace: crossplane-system
source: Secret
kubectl apply -f provider-config.yaml
Deploy the composition
Apply the UserAccessKey XRD and composition. This composition creates an IAM
user and two access keys, writing connection details back to Vault through the
Secrets Proxy:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: useraccesskeys-go-templating
spec:
compositeTypeRef:
apiVersion: example.org/v1alpha1
kind: UserAccessKey
mode: Pipeline
pipeline:
- step: render-templates
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
---
apiVersion: iam.aws.m.upbound.io/v1beta1
kind: User
metadata:
annotations:
{{ setResourceNameAnnotation "user" }}
spec:
forProvider: {}
---
apiVersion: iam.aws.m.upbound.io/v1beta1
kind: AccessKey
metadata:
annotations:
{{ setResourceNameAnnotation "accesskey-0" }}
spec:
forProvider:
userSelector:
matchControllerRef: true
writeConnectionSecretToRef:
name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-0
---
apiVersion: iam.aws.m.upbound.io/v1beta1
kind: AccessKey
metadata:
annotations:
{{ setResourceNameAnnotation "accesskey-1" }}
spec:
forProvider:
userSelector:
matchControllerRef: true
writeConnectionSecretToRef:
name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-1
---
apiVersion: v1
kind: Secret
metadata:
name: {{ dig "spec" "writeConnectionSecretToRef" "name" "" $.observed.composite.resource}}
annotations:
{{ setResourceNameAnnotation "connection-secret" }}
{{ if eq $.observed.resources nil }}
data: {}
{{ else }}
data:
user-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.username }}
user-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.username }}
password-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.password }}
password-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.password }}
{{ end }}
- step: ready
functionRef:
name: function-auto-ready
---
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: useraccesskeys.example.org
spec:
group: example.org
names:
kind: UserAccessKey
plural: useraccesskeys
scope: Namespaced
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
writeConnectionSecretToRef:
type: object
properties:
name:
type: string
kubectl apply -f comp.yaml
Create the composite resource
-
Edit
xr.yamland replace<put-your-initials>with your initials in both themetadata.nameandspec.writeConnectionSecretToRef.namefields:apiVersion: example.org/v1alpha1
kind: UserAccessKey
metadata:
namespace: default
name: <your-initials>-keys
spec:
writeConnectionSecretToRef:
name: <your-initials>-keys-connection-details -
Apply the resource and verify reconciliation:
kubectl apply -f xr.yaml
kubectl get managed
kubectl get compositeConnection details are stored in Vault, not in Kubernetes. Confirm no new secrets were created:
kubectl get secret -n crossplane-system
Clean up
Delete the composite resource. The garbage collector automatically removes the associated secrets from Vault:
kubectl delete -f xr.yaml