PKI as a Service
A partir da v1.8, o CipherVault opera como PKI interna — emite certificados X.509 assinados por CAs internas role-based, com CRL centralizada. Use case principal: mTLS entre microsserviços sem depender de Let's Encrypt + DNS challenges, ou de uma PKI corporativa externa.
Conceitos
- CA — Certificate Authority interna (nome, RSA-2048 self-signed)
- Role — política de issuance (CN regex, DNS regex, TTL, key usages)
- Cert — certificado emitido sob uma role (com serial + revocation tracking)
CA (RSA-2048, self-signed)
├── Role "internal-services-tls" → emite certs serverAuth+clientAuth
├── Role "client-mtls" → emite certs clientAuth-only
└── Role "intermediate-ca" → emite cert is_ca=true (CA filha)
Setup
1. Criar CA
UI: PKI → Nova CA. Ou via API:
curl -X POST https://cv.acme.com.br/pki/cas \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"name": "internal-acme-ca",
"common_name": "Acme Internal CA",
"organization": "Acme Corp",
"country": "BR",
"ttl_days": 3650
}'
A chave privada da CA é cifrada via KMS envelope encryption antes de persistir. Não é exportável.
2. Criar role
curl -X POST https://cv.acme.com.br/pki/roles \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"name": "internal-services-tls",
"ca_id": 1,
"allowed_cn_regex": "^[a-z0-9-]+\\.svc\\.acme\\.internal$",
"allowed_dns_regex": "^[a-z0-9-]+\\.svc\\.acme\\.internal$",
"default_ttl_sec": 86400,
"max_ttl_sec": 7776000,
"key_usages": ["digitalSignature", "keyEncipherment"],
"ext_key_usages": ["serverAuth", "clientAuth"],
"is_ca": false
}'
TTL cap absoluto: 90 dias.
Issuance — dois modos
CSR mode
App gera keypair localmente e envia CSR. Backend valida e assina.
# 1. Gerar key + CSR
openssl req -new -newkey rsa:2048 -nodes \
-keyout app.key -out app.csr \
-subj "/CN=billing-api.svc.acme.internal/O=Acme Corp"
# 2. Submeter
curl -X POST https://cv.acme.com.br/pki/roles/1/issue \
-H "Authorization: Bearer $CV_TOKEN" \
-d "{
\"csr_pem\": \"$(awk '{printf "%s\\n", $0}' app.csr)\",
\"reason\": \"Deploy billing-api\"
}" | jq -r .certificate > app.crt
Chave privada nunca sai do servidor da app.
generateKey mode
Backend gera keypair RSA-2048 e devolve cert + chave privada one-shot (útil para casos onde app não tem ferramentas de geração de CSR).
curl -X POST https://cv.acme.com.br/pki/roles/1/issue \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{
"common_name": "billing-api.svc.acme.internal",
"san_dns": ["billing-api.svc.acme.internal"],
"ttl_sec": 86400,
"reason": "Deploy billing-api"
}'
# Resposta:
# {
# "certificate": "-----BEGIN CERTIFICATE-----...",
# "private_key": "-----BEGIN PRIVATE KEY-----...",
# "ca_chain": "...",
# "serial": "1234567"
# }
⚠️ Backend não persiste a chave privada. Após response, chave existe apenas onde você salvar.
Endpoints públicos (sem auth)
Para distribuição da CA e CRL para servidores que validam certs:
GET /pki/cas/:id/cert Certificado da CA em PEM
GET /pki/cas/:id/crl Lista de revogação (CRL DER)
POST /pki/cas/:id/ocsp OCSP responder RFC 6960 (v3.0+)
GET /pki/cas/:id/ocsp/:b64 OCSP via GET (alternativa)
Hosts/serviços validam certs apresentados contra estes endpoints.
OCSP responder (v3.0+)
Para clientes que validam revogação online (browsers, mTLS rigoroso):
# Cliente envia OCSPRequest DER
openssl ocsp \
-issuer ca.pem \
-cert client-cert.pem \
-url https://cv.acme.com.br/pki/cas/1/ocsp \
-resp_text
Response é BasicOCSPResponse assinada com a chave da CA (lib/ocsp.js).
Hierarquia de CAs
A v3.0 introduz parent_ca_id em pki_cas — permite modelar:
root-ca (self-signed, RSA-4096, 10 anos)
├── intermediate-billing (signed by root, 5 anos)
│ ├── role: services-tls (90d)
│ └── role: client-mtls (30d)
└── intermediate-platform (signed by root, 5 anos)
└── role: services-tls (90d)
curl -X POST https://cv.acme.com.br/pki/cas \
-d '{
"name": "intermediate-billing",
"common_name": "Acme Billing Intermediate CA",
"parent_ca_id": 1,
"ttl_days": 1825
}'
CRL — revogação
curl -X POST https://cv.acme.com.br/pki/certs/1234567/revoke \
-H "Authorization: Bearer $CV_TOKEN" \
-d '{ "reason": "Compromisso suspeitado em billing-api" }'
# Servidor que valida certs, cron 5min:
curl https://cv.acme.com.br/pki/cas/1/crl > /etc/ssl/cv_internal.crl
nginx -s reload # ou whatever
CLI Go cv
cv pki issue \
--role 1 \
--cn billing-api.svc.acme.internal \
--san billing-api.svc.acme.internal \
--ttl 86400 \
--reason "Deploy billing-api" \
--out ./certs/
# escreve ./certs/cert.pem, ./certs/key.pem, ./certs/ca-chain.pem
Terraform Provider
resource "ciphervault_pki_ca" "internal" {
name = "internal-acme-ca"
common_name = "Acme Internal CA"
organization = "Acme Corp"
country = "BR"
ttl_days = 3650
}
resource "ciphervault_pki_role" "internal_services" {
name = "internal-services-tls"
ca_id = ciphervault_pki_ca.internal.id
allowed_cn_regex = "^[a-z0-9-]+\\.svc\\.acme\\.internal$"
allowed_dns_regex = "^[a-z0-9-]+\\.svc\\.acme\\.internal$"
default_ttl_sec = 86400
max_ttl_sec = 7776000
key_usages = ["digitalSignature", "keyEncipherment"]
ext_key_usages = ["serverAuth", "clientAuth"]
is_ca = false
}
Data sources disponíveis: ciphervault_pki_ca_cert (busca PEM da CA).
Implementação
- Algoritmos: RSA-2048 self-signed via
node-forge - Tabelas:
pki_cas(chave privada cifrada),pki_roles(policy),pki_issued_certs(audit + serials) is_caem role permite emitir CAs filhas (intermediate CAs)- CRL atualizada incrementalmente — não re-gera o arquivo a cada revogação
Boas práticas
- CA por ambiente —
internal-prod-ca,internal-staging-ca. Não compartilhe. - Roles estritas —
allowed_cn_regexé mandatório. Sem regex permissivo demais. - TTL razoável — 24h em workloads, 7 dias em servidores estáticos, 90 dias absoluto.
- CRL pull cron 5min em todos os hosts validadores.
- Audit alertas — issuance fora de horário, série de revogations rápidas, etc.
- Rotação de CA — não trivial; planeje com antecedência (gere CA filha, faça crossover, depreca CA antiga).