Deploy SSL Certificates

This role deploys SSL/TLS certificates and private keys from Ansible Vault encrypted variables to target systems.

ARA Ansible Apache Bash Debian HTTPS JSON Nginx

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.

VariableRequiredDescription
deploy_ssl_certificates_listYesList of certificate configurations to deploy

Optional Variables

VariableDefaultDescription
deploy_ssl_certificates_redhat_cert_dir/etc/pki/tls/certsRedHat certificate directory
deploy_ssl_certificates_redhat_key_dir/etc/pki/tls/privateRedHat private key directory
deploy_ssl_certificates_debian_cert_dir/etc/ssl/certsDebian certificate directory
deploy_ssl_certificates_debian_key_dir/etc/ssl/privateDebian private key directory
deploy_ssl_certificates_default_cert_ownerrootDefault certificate file owner
deploy_ssl_certificates_default_cert_grouprootDefault certificate file group
deploy_ssl_certificates_default_cert_mode0644Default certificate file permissions
deploy_ssl_certificates_default_redhat_key_ownerrootDefault RedHat key owner
deploy_ssl_certificates_default_redhat_key_grouprootDefault RedHat key group
deploy_ssl_certificates_default_redhat_key_mode0640Default RedHat key permissions
deploy_ssl_certificates_default_debian_key_ownerrootDefault Debian key owner
deploy_ssl_certificates_default_debian_key_groupssl-certDefault Debian key group
deploy_ssl_certificates_default_debian_key_mode0640Default 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:

FieldRequiredDescription
cert_contentYesCertificate content from vault variable
cert_pathYesDestination path for certificate file
cert_ownerNoCertificate file owner (default: root)
cert_groupNoCertificate file group (default: root)
cert_modeNoCertificate file permissions (default: 0644)
key_contentYesPrivate key content from vault variable
key_pathYesDestination path for private key file
key_ownerNoPrivate key owner (default: root)
key_groupNoPrivate key group (default: OS-specific)
key_modeNoPrivate key permissions (default: 0640)
ca_contentNoCA certificate content from vault
ca_pathNoDestination path for CA certificate
ca_ownerNoCA certificate owner (default: root)
ca_groupNoCA certificate group (default: root)
ca_modeNoCA certificate permissions (default: 0644)
jks_contentNoBase64-encoded JKS keystore from vault
jks_pathNoDestination path for JKS keystore
jks_ownerNoJKS keystore owner (default: root)
jks_groupNoJKS keystore group (default: root)
jks_modeNoJKS 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_password defined 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

  1. Checks OS family (ansible_facts[‘os_family’] == ‘RedHat’)
  2. Deploys certificate files to /etc/pki/tls/certs/ with mode 0644
  3. Deploys private key files to /etc/pki/tls/private/ with mode 0640
  4. Deploys CA certificates (if configured) with mode 0644
  5. Deploys JKS keystores (if configured) with base64 decoding
  6. Sets ownership and permissions as specified

For Debian Systems

  1. Checks OS family (ansible_facts[‘os_family’] == ‘Debian’)
  2. Deploys certificate files to /etc/ssl/certs/ with mode 0644
  3. Deploys private key files to /etc/ssl/private/ with group ssl-cert, mode 0640
  4. Deploys CA certificates (if configured) with mode 0644
  5. Deploys JKS keystores (if configured) with base64 decoding
  6. Sets ownership and permissions as specified

For Proxmox VE (Additional Steps)

  1. Obtains API authentication ticket using root@pam credentials
  2. Reads deployed certificate from filesystem using slurp
  3. Reads deployed private key from filesystem using slurp
  4. Uploads to Proxmox API at /api2/json/nodes/{hostname}/certificates/custom
  5. Forces certificate replacement (force: 1)
  6. Automatically restarts services (restart: 1)
  7. Uses CSRF token for API security

OS-Specific Paths

RedHat/CentOS/Rocky Linux

TypePath
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

TypePath
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

  1. POST to /api2/json/access/ticket

    • Body: username=root@pam&password=...
    • Returns: { "ticket": "...", "CSRFPreventionToken": "..." }
  2. POST to /api2/json/nodes/{hostname}/certificates/custom

    • Cookie: PVEAuthCookie={ticket}
    • Header: CSRFPreventionToken: {token}
    • Body: certificates=...&key=...&force=1&restart=1

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: true to 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_list causes role to skip (no error)
  • JKS keystores must be base64 encoded in vault
  • CA certificates are optional (only deploy if ca_content defined)
  • 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 root on 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

  1. Use Ansible Vault for all certificates and private keys
  2. Separate vault files per environment or host group
  3. Rotate certificates before expiration
  4. Test on single host before deploying widely
  5. Backup private keys separately (encrypted)
  6. Use strong key sizes (RSA 2048+ or ECDSA 256+)
  7. Include intermediate certs in CA chain
  8. Set expiration reminders (certbot does this automatically)
  9. Document certificate sources in comments
  10. Restart services after certificate deployment

Certificate Renewal Workflow

  1. Obtain new certificate (from CA or regenerate self-signed)
  2. Update vault file: ansible-vault edit group_vars/all/vault.yml
  3. Update certificate content in vault variable
  4. Run playbook: ansible-playbook site.yml --tags ssl
  5. Verify deployment: Check application logs
  6. Restart services if needed
  7. 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

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.