Pular para o conteúdo principal

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_ca em 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 ambienteinternal-prod-ca, internal-staging-ca. Não compartilhe.
  • Roles estritasallowed_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).