Deploy SSL Certificates
This role deploys SSL/TLS certificates and private keys from Ansible Vault encrypted variables to target systems.
Deploy SSL Certificates Role
Overview
This role deploys SSL/TLS certificates and private keys from Ansible Vault encrypted variables to target systems. It supports RedHat and Debian-based systems with OS-specific paths and permissions, handles multiple certificate formats (PEM, JKS), includes optional CA certificates, and provides special integration with Proxmox VE via its REST API to register certificates and automatically restart services.
Purpose
- Centralized Certificate Management: Deploy certificates from Ansible Vault to all infrastructure
- Security: Keep private keys encrypted in Ansible Vault, never in plaintext
- Multi-OS Support: Handles RedHat and Debian path conventions automatically
- Format Flexibility: Supports PEM certificates, private keys, CA chains, and JKS keystores
- Proxmox Integration: Registers certificates via Proxmox API with automatic service restart
- Custom Permissions: Configure ownership and permissions per certificate
- Idempotent: Safe to run repeatedly without side effects
Requirements
- Ansible 2.9 or higher
- Certificates and private keys stored in Ansible Vault variables
- Proper sudo/root permissions to write to system certificate directories
- For Proxmox: Root password in Ansible Vault (
vault_proxmox_root_password) - For JKS keystores: Base64-encoded keystore content in vault
Role Variables
Required Variables
Certificate configurations are defined per-host in host_vars files.
| Variable | Required | Description |
|---|---|---|
deploy_ssl_certificates_list | Yes | List of certificate configurations to deploy |
Optional Variables
| Variable | Default | Description |
|---|---|---|
deploy_ssl_certificates_redhat_cert_dir | /etc/pki/tls/certs | RedHat certificate directory |
deploy_ssl_certificates_redhat_key_dir | /etc/pki/tls/private | RedHat private key directory |
deploy_ssl_certificates_debian_cert_dir | /etc/ssl/certs | Debian certificate directory |
deploy_ssl_certificates_debian_key_dir | /etc/ssl/private | Debian private key directory |
deploy_ssl_certificates_default_cert_owner | root | Default certificate file owner |
deploy_ssl_certificates_default_cert_group | root | Default certificate file group |
deploy_ssl_certificates_default_cert_mode | 0644 | Default certificate file permissions |
deploy_ssl_certificates_default_redhat_key_owner | root | Default RedHat key owner |
deploy_ssl_certificates_default_redhat_key_group | root | Default RedHat key group |
deploy_ssl_certificates_default_redhat_key_mode | 0640 | Default RedHat key permissions |
deploy_ssl_certificates_default_debian_key_owner | root | Default Debian key owner |
deploy_ssl_certificates_default_debian_key_group | ssl-cert | Default Debian key group |
deploy_ssl_certificates_default_debian_key_mode | 0640 | Default Debian key permissions |
Variable Details
deploy_ssl_certificates_list
List of certificate configurations with content from Ansible Vault.
RedHat Example:
deploy_ssl_certificates_list:
- cert_content: "{{ vault_apache_cert }}"
cert_path: /etc/pki/tls/certs/apache.crt
cert_owner: root
cert_group: root
cert_mode: '0644'
key_content: "{{ vault_apache_key }}"
key_path: /etc/pki/tls/private/apache.key
key_owner: root
key_group: apache
key_mode: '0640'
ca_content: "{{ vault_ca_chain }}"
ca_path: /etc/pki/tls/certs/ca-chain.pem
Debian Example:
deploy_ssl_certificates_list:
- cert_content: "{{ vault_nginx_cert }}"
cert_path: /etc/ssl/certs/nginx.pem
cert_owner: root
cert_group: root
cert_mode: '0644'
key_content: "{{ vault_nginx_key }}"
key_path: /etc/ssl/private/nginx.key
key_owner: root
key_group: ssl-cert
key_mode: '0640'
JKS Keystore Example:
deploy_ssl_certificates_list:
- cert_content: "{{ vault_java_cert }}"
cert_path: /etc/pki/tls/certs/app.crt
key_content: "{{ vault_java_key }}"
key_path: /etc/pki/tls/private/app.key
jks_content: "{{ vault_java_jks_base64 }}"
jks_path: /etc/pki/tls/certs/keystore.jks
jks_owner: root
jks_group: tomcat
jks_mode: '0644'
Field descriptions:
| Field | Required | Description |
|---|---|---|
cert_content | Yes | Certificate content from vault variable |
cert_path | Yes | Destination path for certificate file |
cert_owner | No | Certificate file owner (default: root) |
cert_group | No | Certificate file group (default: root) |
cert_mode | No | Certificate file permissions (default: 0644) |
key_content | Yes | Private key content from vault variable |
key_path | Yes | Destination path for private key file |
key_owner | No | Private key owner (default: root) |
key_group | No | Private key group (default: OS-specific) |
key_mode | No | Private key permissions (default: 0640) |
ca_content | No | CA certificate content from vault |
ca_path | No | Destination path for CA certificate |
ca_owner | No | CA certificate owner (default: root) |
ca_group | No | CA certificate group (default: root) |
ca_mode | No | CA certificate permissions (default: 0644) |
jks_content | No | Base64-encoded JKS keystore from vault |
jks_path | No | Destination path for JKS keystore |
jks_owner | No | JKS keystore owner (default: root) |
jks_group | No | JKS keystore group (default: root) |
jks_mode | No | JKS keystore permissions (default: 0644) |
Vault Variable Format
Certificates and keys should be stored in Ansible Vault as multi-line strings:
group_vars/all/vault.yml (encrypted):
vault_apache_cert: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ5Z...
...
-----END CERTIFICATE-----
vault_apache_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
...
-----END PRIVATE KEY-----
vault_ca_chain: |
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAA...
...
-----END CERTIFICATE-----
# JKS must be base64 encoded
vault_java_jks_base64: "MIIKZgIBAzCCCh8GCSqGSIb3DQEHA..."
Dependencies
This role has no dependencies on other Ansible roles, but requires:
- Ansible Vault for storing certificates and private keys
- For Proxmox:
vault_proxmox_root_passworddefined in vault
Example Playbook
Basic Usage
---
- name: Deploy SSL Certificates to All Hosts
hosts: all
become: true
roles:
- deploy_ssl_certificates
Deploy to Specific Hosts
---
- name: Deploy Web Server Certificates
hosts: webservers
become: true
roles:
- deploy_ssl_certificates
Deploy to Proxmox with API Registration
---
- name: Deploy Proxmox SSL Certificate
hosts: proxmox
become: true
roles:
- deploy_ssl_certificates
What This Role Does
For RedHat Systems
- Checks OS family (ansible_facts[‘os_family’] == ‘RedHat’)
- Deploys certificate files to
/etc/pki/tls/certs/with mode 0644 - Deploys private key files to
/etc/pki/tls/private/with mode 0640 - Deploys CA certificates (if configured) with mode 0644
- Deploys JKS keystores (if configured) with base64 decoding
- Sets ownership and permissions as specified
For Debian Systems
- Checks OS family (ansible_facts[‘os_family’] == ‘Debian’)
- Deploys certificate files to
/etc/ssl/certs/with mode 0644 - Deploys private key files to
/etc/ssl/private/with group ssl-cert, mode 0640 - Deploys CA certificates (if configured) with mode 0644
- Deploys JKS keystores (if configured) with base64 decoding
- Sets ownership and permissions as specified
For Proxmox VE (Additional Steps)
- Obtains API authentication ticket using root@pam credentials
- Reads deployed certificate from filesystem using slurp
- Reads deployed private key from filesystem using slurp
- Uploads to Proxmox API at
/api2/json/nodes/{hostname}/certificates/custom - Forces certificate replacement (force: 1)
- Automatically restarts services (restart: 1)
- Uses CSRF token for API security
OS-Specific Paths
RedHat/CentOS/Rocky Linux
| Type | Path |
|---|---|
| Certificates | /etc/pki/tls/certs/ |
| Private Keys | /etc/pki/tls/private/ |
| CA Certificates | /etc/pki/tls/certs/ |
Default Permissions:
- Certificates: 0644 (root:root)
- Private Keys: 0640 (root:root)
Debian/Ubuntu
| Type | Path |
|---|---|
| Certificates | /etc/ssl/certs/ |
| Private Keys | /etc/ssl/private/ |
| CA Certificates | /etc/ssl/certs/ |
Default Permissions:
- Certificates: 0644 (root:root)
- Private Keys: 0640 (root:ssl-cert)
Note: Debian uses ssl-cert group for private keys, allowing services to read keys by joining this group.
Proxmox API Integration
Authentication Flow
-
POST to
/api2/json/access/ticket- Body:
username=root@pam&password=... - Returns:
{ "ticket": "...", "CSRFPreventionToken": "..." }
- Body:
-
POST to
/api2/json/nodes/{hostname}/certificates/custom- Cookie:
PVEAuthCookie={ticket} - Header:
CSRFPreventionToken: {token} - Body:
certificates=...&key=...&force=1&restart=1
- Cookie:
Why API Upload for Proxmox?
Proxmox requires certificates to be registered via its API to:
- Update internal certificate database
- Restart proxy services (pveproxy, spiceproxy)
- Enable certificate in web UI (port 8006)
Simply copying files to /etc/pve/ is insufficient - API registration is required.
API Limitations
- Only root@pam user allowed: API tokens cannot upload certificates
- Requires root password: Must be stored in vault
- HTTPS with self-signed cert: Role uses
validate_certs: false
Certificate Formats
PEM Certificates
Standard text format with base64-encoded DER:
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ5Z...
...
-----END CERTIFICATE-----
PEM Private Keys
Can be RSA, ECDSA, or EdDSA:
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
...
-----END PRIVATE KEY-----
Or legacy RSA format:
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
...
-----END RSA PRIVATE KEY-----
CA Certificate Chains
Multiple certificates concatenated:
-----BEGIN CERTIFICATE-----
(Intermediate CA)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(Root CA)
-----END CERTIFICATE-----
JKS Keystores
Java KeyStore format, must be base64 encoded in vault:
# Create base64 for vault
base64 -w 0 keystore.jks > keystore_base64.txt
The role automatically decodes with | b64decode.
Security Considerations
- Encrypted Storage: All certificates/keys stored in Ansible Vault
- No Logging: Tasks use
no_log: trueto prevent exposure - Restricted Permissions: Private keys mode 0640 (not world-readable)
- Group Access: Debian ssl-cert group allows service access
- Proxmox Password: Root password required and vaulted
- HTTPS API: Proxmox API uses HTTPS (even with self-signed cert)
- CSRF Protection: Proxmox API requires CSRF token
Tags
This role does not define any tags. Use playbook-level tags if needed:
- hosts: all
roles:
- deploy_ssl_certificates
tags:
- ssl
- certificates
- security
- https
Notes
- Role is OS-aware and uses appropriate paths automatically
- Empty
deploy_ssl_certificates_listcauses role to skip (no error) - JKS keystores must be base64 encoded in vault
- CA certificates are optional (only deploy if
ca_contentdefined) - Proxmox tasks only run when
inventory_hostname == 'proxmox' - Proxmox API requires valid authentication ticket (2-hour lifetime)
- Role does not restart services except Proxmox (via API
restart: 1) - Certificate renewal requires updating vault and re-running role
Troubleshooting
”Certificate verify failed” errors in applications
Check:
# Verify certificate is valid
openssl x509 -in /etc/pki/tls/certs/apache.crt -noout -text
# Check certificate-key pair match
openssl x509 -noout -modulus -in cert.crt | openssl md5
openssl rsa -noout -modulus -in key.key | openssl md5
# (Should match)
# Verify certificate chain
openssl verify -CAfile /etc/pki/tls/certs/ca-chain.pem cert.crt
“Permission denied” when service reads private key
Solution: Add service user to appropriate group
RedHat:
# If key group is apache
usermod -aG apache service_user
Debian:
# Add to ssl-cert group
usermod -aG ssl-cert service_user
Proxmox API upload fails with 401 Unauthorized
Check:
- Root password correct in vault:
ansible-vault view group_vars/all/vault.yml - Root account not locked:
passwd -S rooton Proxmox - API accessible:
curl -k https://proxmox-ip:8006/api2/json/version
Proxmox certificate not applied after deployment
The role includes restart: 1 in API call, but if service doesn’t restart:
# Manually restart Proxmox web services
systemctl restart pveproxy
systemctl restart spiceproxy
JKS keystore “invalid keystore format” error
Cause: JKS not properly base64 encoded in vault
Fix:
# Re-encode keystore
base64 -w 0 keystore.jks
# Update vault variable with output
ansible-vault edit group_vars/all/vault.yml
Certificate deployed but service still uses old cert
Services need restart. Role does not restart services (except Proxmox via API).
Apache:
systemctl restart httpd # RedHat
systemctl restart apache2 # Debian
Nginx:
systemctl restart nginx
Reload is often sufficient:
systemctl reload httpd
systemctl reload nginx
Creating Ansible Vault Variables
Step 1: Generate or Obtain Certificates
# Option A: Self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt
# Option B: Let's Encrypt
certbot certonly --standalone -d example.com
# Option C: Existing certificates from CA
# (already have .crt and .key files)
Step 2: Create Vault File
# Create encrypted vault file
ansible-vault create group_vars/webservers/vault.yml
Step 3: Add Certificate Content
In the vault editor:
vault_webserver_cert: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ5Z...
(paste entire certificate)
...
-----END CERTIFICATE-----
vault_webserver_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
(paste entire private key)
...
-----END PRIVATE KEY-----
Step 4: Reference in Host Vars
host_vars/webserver1.yml:
deploy_ssl_certificates_list:
- cert_content: "{{ vault_webserver_cert }}"
cert_path: /etc/ssl/certs/webserver.pem
key_content: "{{ vault_webserver_key }}"
key_path: /etc/ssl/private/webserver.key
Best Practices
- Use Ansible Vault for all certificates and private keys
- Separate vault files per environment or host group
- Rotate certificates before expiration
- Test on single host before deploying widely
- Backup private keys separately (encrypted)
- Use strong key sizes (RSA 2048+ or ECDSA 256+)
- Include intermediate certs in CA chain
- Set expiration reminders (certbot does this automatically)
- Document certificate sources in comments
- Restart services after certificate deployment
Certificate Renewal Workflow
- Obtain new certificate (from CA or regenerate self-signed)
- Update vault file:
ansible-vault edit group_vars/all/vault.yml - Update certificate content in vault variable
- Run playbook:
ansible-playbook site.yml --tags ssl - Verify deployment: Check application logs
- Restart services if needed
- Test HTTPS:
curl -v https://hostname
Monitoring Certificate Expiration
# Check certificate expiration date
openssl x509 -in /etc/pki/tls/certs/apache.crt -noout -enddate
# Check all certificates in directory
for cert in /etc/pki/tls/certs/*.crt; do
echo "Certificate: $cert"
openssl x509 -in "$cert" -noout -enddate
done
Consider using monitoring tools:
- Nagios: check_http with certificate checking
- Prometheus: blackbox_exporter with TLS probe
- Certbot: Automatic renewal for Let’s Encrypt
Related Roles
This role is often used with:
- deploy_system_monitoring: Configure monitoring for certificate expiration
- Apache/Nginx configuration roles: Configure web servers to use deployed certificates
- Firewall roles: Open HTTPS ports (443)
License
MIT
Author
Created for homelab infrastructure management.