This is part of the Vault + Kubernetes Integration Guide. Return to the main guide for the full architecture overview.
The ArgoCD Vault Plugin (AVP) bridges the gap between GitOps and secrets management. It processes your Kubernetes manifests at sync time, replacing placeholder tokens with real values from Vault — so you never commit actual secrets to Git.
The Problem AVP Solves
WITHOUT AVP:
┌──────────┐ ┌─────────────┐
│ Git │ ❌ Real secrets │ Kubernetes │
│ │──────────────────►│ │
│ │ committed to Git │ │
└──────────┘ └─────────────┘
WITH AVP:
┌──────────┐ Placeholders ┌──────────┐ Resolved ┌─────────────┐
│ Git │──────────────►│ ArgoCD │──────────►│ Kubernetes │
│ <secret> │ │ + AVP │ │ (real vals) │
└──────────┘ └────┬─────┘ └─────────────┘
│
┌────▼─────┐
│ Vault │
└──────────┘
Installation
Method 1: InitContainer (Recommended)
Patch the argocd-repo-server to download AVP at startup:
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-repo-server
namespace: argocd
spec:
template:
spec:
serviceAccountName: argocd-repo-server
initContainers:
- name: download-avp
image: alpine:3.19
command: [sh, -c]
args:
- |
wget -q -O /custom-tools/argocd-vault-plugin \
https://github.com/argoproj-labs/argocd-vault-plugin/releases/download/v1.18.1/argocd-vault-plugin_1.18.1_linux_amd64
chmod +x /custom-tools/argocd-vault-plugin
volumeMounts:
- name: custom-tools
mountPath: /custom-tools
containers:
- name: argocd-repo-server
volumeMounts:
- name: custom-tools
mountPath: /usr/local/bin/argocd-vault-plugin
subPath: argocd-vault-plugin
envFrom:
- secretRef:
name: argocd-vault-plugin-credentials
volumes:
- name: custom-tools
emptyDir: {}
Method 2: Sidecar Plugin (ArgoCD 2.6+)
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: argocd-vault-plugin
spec:
allowConcurrency: true
discover:
find:
command: [sh, -c]
args: ["find . -name '*.yaml' | head -1"]
generate:
command: [argocd-vault-plugin]
args: [generate, ./]
lockRepo: false
Plugin Registration
Register AVP in the argocd-cm ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
configManagementPlugins: |
- name: argocd-vault-plugin
generate:
command: ["argocd-vault-plugin"]
args: ["generate", "./"]
- name: argocd-vault-plugin-helm
init:
command: [sh, -c]
args: ["helm dependency build"]
generate:
command: [sh, -c]
args: ["helm template $ARGOCD_APP_NAME -n $ARGOCD_APP_NAMESPACE . | argocd-vault-plugin generate -"]
- name: argocd-vault-plugin-kustomize
generate:
command: [sh, -c]
args: ["kustomize build . | argocd-vault-plugin generate -"]
Vault Authentication for AVP
Option A: Kubernetes Auth (Recommended)
apiVersion: v1
kind: Secret
metadata:
name: argocd-vault-plugin-credentials
namespace: argocd
type: Opaque
stringData:
VAULT_ADDR: "https://vault.corp.com:8200"
AVP_TYPE: "vault"
AVP_AUTH_TYPE: "k8s"
AVP_K8S_MOUNT_PATH: "auth/kubernetes"
AVP_K8S_ROLE: "argocd-avp-role"
Configure Vault:
# Create a policy for ArgoCD
vault policy write argocd-avp-policy - <<EOF
path "secret/data/*" {
capabilities = ["read"]
}
EOF
# Create a role for the ArgoCD repo-server SA
vault write auth/kubernetes/role/argocd-avp-role \
bound_service_account_names=argocd-repo-server \
bound_service_account_namespaces=argocd \
policies=argocd-avp-policy \
ttl=1h
Option B: AppRole Auth
apiVersion: v1
kind: Secret
metadata:
name: argocd-vault-plugin-credentials
namespace: argocd
type: Opaque
stringData:
VAULT_ADDR: "https://vault.corp.com:8200"
AVP_TYPE: "vault"
AVP_AUTH_TYPE: "approle"
AVP_ROLE_ID: "<role-id>"
AVP_SECRET_ID: "<secret-id>"
Option C: Token Auth (Dev/Testing Only)
stringData:
VAULT_ADDR: "https://vault.corp.com:8200"
AVP_TYPE: "vault"
AVP_AUTH_TYPE: "token"
VAULT_TOKEN: "<vault-token>"
Using Placeholders in Manifests
Basic Placeholder Syntax
apiVersion: v1
kind: Secret
metadata:
name: webapp-secret
annotations:
avp.kubernetes.io/path: "secret/data/webapp/config"
type: Opaque
stringData:
db_host: <db_host>
db_user: <db_user>
db_pass: <db_pass>
AVP replaces <db_host> with the value of key db_host at path secret/data/webapp/config.
Per-Key Path Override
stringData:
db_pass: <path:secret/data/webapp/db#password>
api_key: <path:secret/data/webapp/api#key>
Versioned Secrets (KV v2)
stringData:
db_pass: <path:secret/data/webapp/config#db_pass#3>
# Reads version 3 of the secret
ConfigMaps with Placeholders
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
annotations:
avp.kubernetes.io/path: "secret/data/webapp/config"
data:
config.yaml: |
database:
host: <db_host>
port: "5432"
name: <db_name>
redis:
url: <redis_url>
Helm Values with AVP
In your Git repo, create a Helm values file with placeholders:
# values.yaml
database:
host: <path:secret/data/webapp/config#db_host>
password: <path:secret/data/webapp/config#db_pass>
ingress:
tls:
secretName: <path:secret/data/webapp/tls#cert_name>
Creating the ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: webapp
namespace: argocd
spec:
project: default
destination:
server: https://kubernetes.default.svc
namespace: webapp-ns
source:
repoURL: https://github.com/your-org/k8s-manifests.git
path: apps/webapp
plugin:
name: argocd-vault-plugin
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
For Helm with AVP:
source:
repoURL: https://github.com/your-org/helm-charts.git
path: charts/webapp
plugin:
name: argocd-vault-plugin-helm
Refreshing Secrets
Secrets are resolved at sync time. To force a re-fetch:
# Hard refresh via CLI
argocd app get webapp --hard-refresh
# Or trigger a sync
argocd app sync webapp
For automated rotation, set up a CronJob that triggers ArgoCD syncs:
apiVersion: batch/v1
kind: CronJob
metadata:
name: refresh-webapp-secrets
namespace: argocd
spec:
schedule: "0 */4 * * *" # Every 4 hours
jobTemplate:
spec:
template:
spec:
containers:
- name: argocd-sync
image: argoproj/argocd:v2.12
command: [argocd]
args:
- app
- sync
- webapp
- --server
- argocd-server.argocd.svc
- --auth-token
- $(ARGOCD_TOKEN)
restartPolicy: OnFailure
Multi-Environment Setup
Structure your Git repo for environment isolation:
k8s-manifests/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
├── overlays/
│ ├── dev/
│ │ ├── secret.yaml # avp.kubernetes.io/path: "secret/data/dev/webapp"
│ │ └── kustomization.yaml
│ ├── staging/
│ │ ├── secret.yaml # avp.kubernetes.io/path: "secret/data/staging/webapp"
│ │ └── kustomization.yaml
│ └── prod/
│ ├── secret.yaml # avp.kubernetes.io/path: "secret/data/prod/webapp"
│ └── kustomization.yaml
Create separate ArgoCD Applications per environment, each using argocd-vault-plugin-kustomize.
Troubleshooting
“could not find placeholder”
- Check that
avp.kubernetes.io/pathannotation matches a real Vault path - Verify the secret exists:
vault kv get secret/webapp/config
“permission denied”
- Check that the AVP auth role has the correct policy attached
- Verify the ArgoCD repo-server ServiceAccount is correctly bound
Plugin not executing
# Check repo-server logs
kubectl logs -n argocd deployment/argocd-repo-server
# Verify plugin binary exists
kubectl exec -n argocd deployment/argocd-repo-server -- \
argocd-vault-plugin version
Return to the main Vault + Kubernetes guide for the complete architecture overview and auth method comparison.