TLS para NGINX con Vault Agent: prolijo y sin drama
Guia para configurar Vault Agent en RHEL 9 o 10, leer un certificado guardado en
HashiCorp Vault y dejarlo listo para que NGINX lo use como certificado TLS.
La idea es simple: que el servidor no ande recibiendo certificados a mano.
Vault Agent se autentica con AppRole, lee el secreto, escribe el .crt y la
.key, valida NGINX y hace reload cuando hay cambios. Menos copia manual,
menos chances de romper algo un viernes a la tarde. Tu yo del futuro te lo va a
agradecer.
No subas certificados, claves privadas, tokens,
role_id,secret_idni
archivos renderizados al repositorio. Parece obvio, pero estas cosas pasan.
Probado con
- RHEL 9
- Deberia aplicar igual en RHEL 10, validando instalacion del paquete
vault - Vault Agent instalado desde el repositorio oficial de HashiCorp
- HashiCorp Vault con auth method AppRole
- Secret engine KV v2
- NGINX usando certificados locales
- systemd para ejecutar Vault Agent como servicio
Prerrequisitos
Antes de arrancar, revisa que tengas esto a mano:
- Un Vault accesible desde el servidor RHEL.
- Un secret engine KV v2 con el certificado y la private key.
- Un usuario con permisos para crear policies y AppRoles en Vault.
- NGINX instalado en el servidor RHEL.
- Acceso
sudoen el servidor RHEL.
Variables de ejemplo
Ajusta estos valores al entorno antes de copiar los comandos. Parece detalle,
pero te evita renegar despues con rutas o nombres cruzados.
export VAULT_ADDR="https://"
export KV_MOUNT="certificados"
export CERT_NAME=""
export VAULT_AGENT_ROLE="vault-agent-${CERT_NAME}"
export VAULT_POLICY_NAME="${CERT_NAME}-cert-policy"
export VAULT_AGENT_USER="vault-agent"
export VAULT_AGENT_DIR="/etc/vault-agent.d"
export VAULT_AGENT_STATE_DIR="/var/lib/vault-agent"
export VAULT_AGENT_BASE_DIR="/opt/vault-agent"
export KV_SECRET_PATH="${KV_MOUNT}/data/${CERT_NAME}"
export KV_METADATA_PATH="${KV_MOUNT}/metadata/${CERT_NAME}"
Ejemplo de secreto KV v2 esperado:
certificados/
Campos esperados dentro del secreto:
certificate
private_key
Si los campos tienen otros nombres, ajusta .Data.data.certificate y
.Data.data.private_key en los templates.
Instalar Vault
Instala el repositorio oficial de HashiCorp y el binario vault. El mismo
binario tambien sirve para correr Vault Agent.
sudo dnf install -y yum-utils
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo dnf install -y vault
Valida que quedo instalado:
vault version
which vault
Y proba que el subcomando de Agent este disponible:
vault agent -h
Crear usuario y directorios
Crea un usuario dedicado para el agente. Es mejor que Vault Agent no corra como
root porque si algo sale torcido, el radio de daño queda mas chico.
sudo useradd --system \
--home "${VAULT_AGENT_DIR}" \
--shell /sbin/nologin \
"${VAULT_AGENT_USER}"
Crea la estructura de trabajo:
sudo install -d -o "${VAULT_AGENT_USER}" -g "${VAULT_AGENT_USER}" -m 750 "${VAULT_AGENT_DIR}"
sudo install -d -o "${VAULT_AGENT_USER}" -g "${VAULT_AGENT_USER}" -m 750 "${VAULT_AGENT_STATE_DIR}"
sudo install -d -o "${VAULT_AGENT_USER}" -g "${VAULT_AGENT_USER}" -m 750 "${VAULT_AGENT_BASE_DIR}/templates"
sudo install -d -o "${VAULT_AGENT_USER}" -g "${VAULT_AGENT_USER}" -m 750 "${VAULT_AGENT_BASE_DIR}/rendered"
Revisa permisos y padres de directorios, para no perder tiempo despues con un
permission denied escondido:
namei -l "${VAULT_AGENT_DIR}" "${VAULT_AGENT_STATE_DIR}" "${VAULT_AGENT_BASE_DIR}"
Validar conectividad con Vault
Proba conectividad contra el endpoint de health:
curl -k "${VAULT_ADDR}/v1/sys/health"
Salida esperada, con valores que pueden variar segun el estado del cluster:
{
"initialized": true,
"sealed": false,
"standby": false
}
El -k sirve para pruebas cuando el servidor todavia no confia en la CA de
Vault. Para produccion conviene instalar la CA y quitar tls_skip_verify.
Crear policy en Vault
Como el secreto esta en un engine KV v2, la lectura se hace sobre
. Para metadata se usa .
Crea la policy:
vault policy write "${VAULT_POLICY_NAME}" - <<POLICY
path "${KV_SECRET_PATH}" {
capabilities = ["read"]
}
path "${KV_METADATA_PATH}" {
capabilities = ["read", "list"]
}
POLICY
Valida que haya quedado como esperabas:
vault policy read "${VAULT_POLICY_NAME}"
Crear AppRole
Crea un AppRole para el agente:
vault write "auth/approle/role/${VAULT_AGENT_ROLE}" \
token_policies="${VAULT_POLICY_NAME}" \
token_ttl="1h" \
token_max_ttl="4h"
Obtene el role_id:
vault read "auth/approle/role/${VAULT_AGENT_ROLE}/role-id"
Genera un secret_id:
vault write -f "auth/approle/role/${VAULT_AGENT_ROLE}/secret-id"
Guarda role_id y secret_id de forma segura. Esos valores se copian al RHEL
y no deben quedar versionados.
Cargar credenciales de AppRole
En el servidor RHEL, crea los archivos que va a leer Vault Agent:
sudo tee "${VAULT_AGENT_DIR}/role_id" > /dev/null <<'ROLEID'
ROLEID
sudo tee "${VAULT_AGENT_DIR}/secret_id" > /dev/null <<'SECRETID'
SECRETID
Ajusta permisos. Estos archivos son sensibles, asi que aca conviene ser
prolijo:
sudo chown "${VAULT_AGENT_USER}:${VAULT_AGENT_USER}" \
"${VAULT_AGENT_DIR}/role_id" \
"${VAULT_AGENT_DIR}/secret_id"
sudo chmod 600 \
"${VAULT_AGENT_DIR}/role_id" \
"${VAULT_AGENT_DIR}/secret_id"
Valida:
sudo ls -l "${VAULT_AGENT_DIR}/role_id" "${VAULT_AGENT_DIR}/secret_id"
Crear templates
Crea el template del certificado:
sudo tee "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-cert.ctmpl" > /dev/null <<EOF
{{- with secret "${KV_SECRET_PATH}" -}}
{{ .Data.data.certificate }}
{{- end }}
EOF
Crea el template de la private key:
sudo tee "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-key.ctmpl" > /dev/null <<EOF
{{- with secret "${KV_SECRET_PATH}" -}}
{{ .Data.data.private_key }}
{{- end }}
EOF
Crea un tercer template para disparar el reload de NGINX una sola vez por
cambio. Asi evitas ejecutar el reload tanto por el certificado como por la key.
sudo tee "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-reload.ctmpl" > /dev/null <<EOF
{{- with secret "${KV_SECRET_PATH}" -}}
version={{ .Data.metadata.version }}
updated_time={{ .Data.metadata.updated_time }}
{{- end }}
EOF
Ajusta permisos:
sudo chown "${VAULT_AGENT_USER}:${VAULT_AGENT_USER}" "${VAULT_AGENT_BASE_DIR}/templates/"*.ctmpl
sudo chmod 640 "${VAULT_AGENT_BASE_DIR}/templates/"*.ctmpl
Configurar Vault Agent
Crea la configuracion principal:
sudo tee "${VAULT_AGENT_DIR}/vault-agent.hcl" > /dev/null <<EOF
pid_file = "${VAULT_AGENT_STATE_DIR}/vault-agent.pid"
vault {
address = "${VAULT_ADDR}"
tls_skip_verify = true
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "${VAULT_AGENT_DIR}/role_id"
secret_id_file_path = "${VAULT_AGENT_DIR}/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "${VAULT_AGENT_STATE_DIR}/token"
mode = 0600
}
}
}
template_config {
static_secret_render_interval = "5m"
}
template {
source = "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-cert.ctmpl"
destination = "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.crt"
perms = 0644
}
template {
source = "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-key.ctmpl"
destination = "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.key"
perms = 0600
}
template {
source = "${VAULT_AGENT_BASE_DIR}/templates/${CERT_NAME}-reload.ctmpl"
destination = "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.reload"
perms = 0644
command = "/usr/local/sbin/reload-nginx-if-valid.sh"
}
EOF
Ajusta permisos:
sudo chown "${VAULT_AGENT_USER}:${VAULT_AGENT_USER}" "${VAULT_AGENT_DIR}/vault-agent.hcl"
sudo chmod 640 "${VAULT_AGENT_DIR}/vault-agent.hcl"
static_secret_render_interval = "5m" hace que Vault Agent vuelva a consultar
secretos estaticos, como KV v2, cada 5 minutos aproximadamente. Ideal para
laboratorio y tambien bastante claro para operar.
Probar Vault Agent a mano
Antes de crear el servicio systemd, conviene probar el agente en primer plano.
Si algo no levanta, arranca mirando logs y errores aca, donde todo queda a la
vista.
sudo -u "${VAULT_AGENT_USER}" env VAULT_ADDR="${VAULT_ADDR}" \
vault agent -config="${VAULT_AGENT_DIR}/vault-agent.hcl"
En otra terminal, valida los archivos renderizados:
sudo ls -l "${VAULT_AGENT_BASE_DIR}/rendered/"
sudo openssl x509 -in "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.crt" -noout -subject -issuer -dates
sudo openssl pkey -in "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.key" -noout -check
Valida que el certificado y la key correspondan:
sudo openssl x509 -noout -modulus -in "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.crt" | openssl md5
sudo openssl rsa -noout -modulus -in "${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.key" | openssl md5
Los hashes deben coincidir.
Crear script de reload para NGINX
El script valida que el certificado y la key existan, prueba la configuracion de
NGINX y recarga el servicio si todo esta bien.
Este script puede recargar NGINX. No deberia cortar conexiones normales, pero
igual conviene probarlo primero en laboratorio o ventana controlada.
sudo tee /usr/local/sbin/reload-nginx-if-valid.sh > /dev/null <<EOF
#!/bin/bash
set -euo pipefail
CERT="${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.crt"
KEY="${VAULT_AGENT_BASE_DIR}/rendered/${CERT_NAME}.key"
test -s "\$CERT"
test -s "\$KEY"
/usr/bin/openssl x509 -in "\$CERT" -noout >/dev/null
/usr/bin/openssl pkey -in "\$KEY" -noout >/dev/null
/usr/bin/sudo /usr/sbin/nginx -t
/usr/bin/sudo /bin/systemctl reload nginx
EOF
Ajusta permisos:
sudo chown root:root /usr/local/sbin/reload-nginx-if-valid.sh
sudo chmod 755 /usr/local/sbin/reload-nginx-if-valid.sh
sudo restorecon -v /usr/local/sbin/reload-nginx-if-valid.sh
Permitir reload con sudoers
Vault Agent corre con un usuario sin privilegios. Para que pueda validar y
recargar NGINX, permiti solo los comandos necesarios.
Ojo aca: sudoers mal escrito te puede dejar sin
sudo. Por eso usamos
visudo, que valida antes de guardar y te ahorra un mal rato.
sudo visudo -f /etc/sudoers.d/vault-agent-nginx
Contenido:
vault-agent ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /bin/systemctl reload nginx
Valida sudoers:
sudo visudo -c
Proba el script como vault-agent:
sudo -u "${VAULT_AGENT_USER}" /usr/local/sbin/reload-nginx-if-valid.sh
Crear servicio systemd
Crea el unit:
sudo tee /etc/systemd/system/vault-agent.service > /dev/null <<EOF
[Unit]
Description=HashiCorp Vault Agent
Requires=network-online.target
After=network-online.target
[Service]
User=${VAULT_AGENT_USER}
Group=${VAULT_AGENT_USER}
Environment="VAULT_ADDR=${VAULT_ADDR}"
ExecStart=/usr/bin/vault agent -config=${VAULT_AGENT_DIR}/vault-agent.hcl
Restart=on-failure
RestartSec=5
KillSignal=SIGINT
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=${VAULT_AGENT_STATE_DIR} ${VAULT_AGENT_BASE_DIR}/rendered
[Install]
WantedBy=multi-user.target
EOF
No uses
NoNewPrivileges=truesi el script necesita ejecutarsudo.
Con esa opcion activa,sudono puede elevar privilegios dentro del servicio.
Activa el servicio. Esto recarga systemd, habilita Vault Agent al arranque y lo
levanta en el momento:
sudo systemctl daemon-reload
sudo systemctl enable --now vault-agent
Valida:
sudo systemctl status vault-agent
sudo journalctl -u vault-agent -f
Configurar NGINX
En el vhost de NGINX, apunta al certificado y la key renderizados por Vault
Agent:
ssl_certificate /opt/vault-agent/rendered/.crt;
ssl_certificate_key /opt/vault-agent/rendered/.key;
Valida y recarga:
Este reload aplica la configuracion nueva de NGINX. Primero
nginx -t,
despues reload. Ese orden es el cinturon de seguridad.
sudo nginx -t
sudo systemctl reload nginx
Verificacion
Revisa estado de Vault Agent:
sudo systemctl status vault-agent
sudo journalctl -u vault-agent -n 100 --no-pager
Revisa archivos renderizados:
sudo ls -l /opt/vault-agent/rendered/
Valida certificado y key:
sudo openssl x509 -in "/opt/vault-agent/rendered/.crt" -noout -subject -issuer -dates
sudo openssl pkey -in "/opt/vault-agent/rendered/.key" -noout -check
Valida NGINX:
sudo nginx -t
systemctl status nginx
Proba reload como el usuario del agente:
sudo -u vault-agent /usr/local/sbin/reload-nginx-if-valid.sh
Cuando se actualiza el secreto en Vault, el flujo esperado es:
- Vault Agent vuelve a consultar el KV v2 en el siguiente ciclo.
- Renderiza el certificado, la key y el archivo
.reload. - Ejecuta
/usr/local/sbin/reload-nginx-if-valid.sh. - El script corre
nginx -t. - Si la configuracion es valida, recarga NGINX.
Confiar en la CA de Vault
tls_skip_verify = true sirve para pruebas, pero no deberia quedar como estado
final. Lo recomendable es instalar la CA interna en RHEL.
sudo cp <ca-vault>.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
Valida sin -k:
curl "${VAULT_ADDR}/v1/sys/health"
Despues, quita tls_skip_verify de la configuracion:
vault {
address = "https://"
}
Reinicia el agente:
Reiniciar Vault Agent corta y vuelve a levantar el proceso. Si algo no vuelve,
revisajournalctl -u vault-agent.
sudo systemctl restart vault-agent
Troubleshooting
Si algo no levanta, arranca mirando logs. En este caso, journalctl y los
mensajes de Vault Agent suelen cantar bastante rapido por donde viene el tema.
configuring the api_proxy requires at least 1 listener
Puede aparecer si se agrega un bloque cache sin configurar listeners.
Si Vault Agent solo se usa para renderizar templates, quita este bloque:
cache {
use_auto_auth_token = true
}
VAULT_ADDR and -address unset
Defini el bloque vault en vault-agent.hcl:
vault {
address = "https://"
}
Tambien podes dejar VAULT_ADDR en el unit de systemd:
Environment="VAULT_ADDR=https://"
fork/exec /tmp/reload-nginx.sh: no such file or directory
El command del template apunta a un script que no existe o que esta en una
ruta temporal.
Usa una ruta estable:
/usr/local/sbin/reload-nginx-if-valid.sh
permission denied al ejecutar el script
Valida permisos, SELinux y acceso a directorios padres:
ls -lZ /usr/local/sbin/reload-nginx-if-valid.sh
namei -l /usr/local/sbin/reload-nginx-if-valid.sh
sudo -u vault-agent /usr/local/sbin/reload-nginx-if-valid.sh
getenforce
Correccion tipica:
sudo chown root:root /usr/local/sbin/reload-nginx-if-valid.sh
sudo chmod 755 /usr/local/sbin/reload-nginx-if-valid.sh
sudo restorecon -v /usr/local/sbin/reload-nginx-if-valid.sh
Error de sudo con NoNewPrivileges=true
Si systemd tiene NoNewPrivileges=true, sudo no puede elevar privilegios
dentro del servicio.
Quita esa linea del unit y recarga systemd:
sudo systemctl daemon-reload
sudo systemctl restart vault-agent
Referencias
- HashiCorp Vault Agent
- HashiCorp Vault install
- HashiCorp Vault AppRole auth method
- HashiCorp Vault KV secrets engine v2
- NGINX SSL module
- Red Hat RHEL 10: Managing sudo access
- Red Hat RHEL 10: Using shared system certificates
Bitacora de supervivencia
Si este apunte te dio una mano y te salvo de genegar un rato, podes encontrar mas material para infra, laboratorio y herramientas de todos los dias en ignaciobustamante.ar.
