Workload Identity
A partir da v1.8, workloads não-humanos (pods K8s, instâncias EC2, funções Cloud Run, App Service) se autenticam no CipherVault apresentando identidade nativa do provedor (sem secret pré-compartilhado).
O CV troca a claim externa por um JWT CV de 1h vinculado a uma AppConnection. A partir daí, fluxo padrão (mTLS+DPoP).
Métodos suportados
| Tipo | Como valida | Bind rules | Versão |
|---|---|---|---|
k8s_sa | TokenReview API K8s | allowed_namespaces, allowed_sa_names | 1.8 |
aws_iam | STS:GetCallerIdentity preassinado | allowed_arns (regex) | 1.8 |
gcp_iam | JWT audience-bound + JWKS Google | expected_audience, allowed_emails | 1.8 |
azure_msi | JWT IMDS + JWKS tenant Azure AD | expected_audience, allowed_object_ids | 1.8 |
oidc_generic | Discovery .well-known/openid-configuration + JWKS cache 24h | expected_issuer, expected_audience, allowed_subjects (regex) | 3.0 |
spiffe | JWT-SVID com trust_domain bundle | trust_domain, allowed_spiffe_ids (regex) | 3.0 |
oidc_generic cobre HashiCorp Vault JWT auth, Okta, Auth0, Keycloak,
qualquer provider OIDC compliant. spiffe cobre clusters SPIRE/Istio
com SPIFFE IDs.
Endpoint público
POST /workload-identity/login
Sem autenticação prévia — o body apresenta credencial nativa do provider:
curl -X POST https://cv.acme.com.br/workload-identity/login \
-H "Content-Type: application/json" \
-d '{
"method_id": 12,
"k8s_token": "<JWT do projected ServiceAccount token>"
}'
# Resposta:
# {
# "cv_jwt": "eyJ...", // válido por 1h, vinculado a AppConnection
# "expires_at": "2026-05-05T15:00:00Z"
# }
Configuração — Kubernetes ServiceAccount
1. Criar method
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"name": "billing-api-prd-k8s",
"type": "k8s_sa",
"app_connection_id": 42,
"config": {
"issuer": "https://kubernetes.default.svc",
"ca_cert_pem": "-----BEGIN CERTIFICATE-----...",
"allowed_namespaces": ["billing"],
"allowed_sa_names": ["billing-api"]
}
}'
ca_cert_pem é o cert do API server. Em GKE/EKS, está disponível no
TokenReview API endpoint.
2. Pod com projected ServiceAccount token
apiVersion: v1
kind: ServiceAccount
metadata:
name: billing-api
namespace: billing
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: billing-api
spec:
template:
spec:
serviceAccountName: billing-api
containers:
- name: api
image: acme/billing-api:1.4.0
volumeMounts:
- name: cv-token
mountPath: /var/run/cv
readOnly: true
volumes:
- name: cv-token
projected:
sources:
- serviceAccountToken:
path: token
audience: https://cv.acme.com.br
expirationSeconds: 3600
3. App troca token
import requests, json, time
def get_cv_jwt():
with open("/var/run/cv/token") as f:
k8s_token = f.read().strip()
r = requests.post(
"https://cv.acme.com.br/workload-identity/login",
json={"method_id": 12, "k8s_token": k8s_token},
)
return r.json()["cv_jwt"]
Configuração — AWS IAM
1. Method
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"name": "etl-job-aws",
"type": "aws_iam",
"app_connection_id": 42,
"config": {
"allowed_arns": [
"arn:aws:iam::123456789012:role/billing-etl-prd"
]
}
}'
2. App apresenta requisição preassinada
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
import requests
session = boto3.Session()
credentials = session.get_credentials().get_frozen_credentials()
# Constrói GetCallerIdentity preassinado
sts_url = "https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15"
request = AWSRequest(method="GET", url=sts_url, headers={
"host": "sts.amazonaws.com",
"x-cv-audience": "https://cv.acme.com.br",
})
SigV4Auth(credentials, "sts", "us-east-1").add_auth(request)
# Passa headers preassinados pro CV
r = requests.post(
"https://cv.acme.com.br/workload-identity/login",
json={
"method_id": 13,
"aws_signed_url": str(request.url),
"aws_signed_headers": dict(request.headers),
},
)
cv_jwt = r.json()["cv_jwt"]
CV re-executa a request preassinada contra sts.amazonaws.com,
valida que o ARN retornado bate com allowed_arns.
Configuração — GCP IAM
# Method
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-d '{
"name": "cloud-run-jobs",
"type": "gcp_iam",
"app_connection_id": 42,
"config": {
"expected_audience": "https://cv.acme.com.br",
"allowed_emails": ["billing-job@my-project.iam.gserviceaccount.com"]
}
}'
# App (Cloud Run)
curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://cv.acme.com.br" \
-H "Metadata-Flavor: Google" > /tmp/gcp_jwt
curl -X POST https://cv.acme.com.br/workload-identity/login \
-d "{\"method_id\": 14, \"gcp_jwt\": \"$(cat /tmp/gcp_jwt)\"}"
Configuração — OIDC genérico (v3.0+)
Para qualquer provider OIDC compliant (Vault, Okta, Auth0, Keycloak):
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"name": "vault-jwt-prd",
"type": "oidc_generic",
"app_connection_id": 42,
"config": {
"expected_issuer": "https://vault.acme.com.br/v1/identity/oidc",
"expected_audience": "https://cv.acme.com.br",
"allowed_subjects": ["^role:billing-job-prd$"]
}
}'
# App troca JWT
curl -X POST https://cv.acme.com.br/workload-identity/login \
-d '{ "method_id": 16, "oidc_token": "<JWT do Vault/Okta/Auth0>" }'
Discovery automática via .well-known/openid-configuration. JWKS
cacheado por 24h.
Configuração — SPIFFE/SPIRE (v3.0+)
Para workloads em clusters SPIRE / Istio com SPIFFE IDs:
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-d '{
"name": "spire-prd",
"type": "spiffe",
"app_connection_id": 42,
"config": {
"trust_domain": "acme.com.br",
"trust_bundle_pem": "-----BEGIN CERTIFICATE-----...",
"expected_audience": "https://cv.acme.com.br",
"allowed_spiffe_ids": [
"^spiffe://acme\\.com\\.br/ns/billing/sa/api$"
]
}
}'
# App apresenta JWT-SVID
JWT_SVID=$(spire-agent api fetch jwt --audience https://cv.acme.com.br | jq -r .svid)
curl -X POST https://cv.acme.com.br/workload-identity/login \
-d "{ \"method_id\": 17, \"jwt_svid\": \"$JWT_SVID\" }"
Configuração — Azure MSI
# Method
curl -X POST https://cv.acme.com.br/workload-identity/methods \
-d '{
"name": "app-service-prd",
"type": "azure_msi",
"app_connection_id": 42,
"config": {
"expected_audience": "https://cv.acme.com.br",
"allowed_object_ids": ["abc123-..."]
}
}'
# App (App Service)
curl "${IDENTITY_ENDPOINT}?resource=https://cv.acme.com.br&api-version=2019-08-01" \
-H "X-IDENTITY-HEADER: ${IDENTITY_HEADER}" > /tmp/azure_jwt
curl -X POST https://cv.acme.com.br/workload-identity/login \
-d "{\"method_id\": 15, \"azure_jwt\": \"$(jq -r .access_token /tmp/azure_jwt)\"}"
Boas práticas
- Bind rules estritas —
allowed_namespaces,allowed_arns,allowed_emailssempre populados - Audience match — o
audiencedo token externo deve ser exatamentehttps://cv.acme.com.br(não*.acme.com.br) - Method por workload — não compartilhe
method_identre apps. Audit fica claro - TTL JWT CV — 1h é suficiente para a maioria dos casos. App pega novo quando expira
- Não cache JWT CV agressivamente — se a credencial nativa for revogada (instance terminada, SA deletado), o JWT CV precisa expirar para refletir
- Combine com Approvals —
mfa_disable,siem_change,rbac_changeexigem dual-control mesmo via Workload Identity