SSL Certificate Automation Management — Ansible Batch Deployment and Update Practices

In modern IT infrastructure management, secure management of SSL certificates has become increasingly important. As the number of services grows and certificate renewal cycles shorten, manual certificate management becomes inefficient and error-prone. This article details how to use Ansible for automated batch deployment and renewal of certificates, improving management efficiency and reducing security risks.

The Importance of SSL Certificate Management

SSL certificates are the foundation of network communication security. They ensure:

  1. Data Encryption: Prevents sensitive data from being intercepted during transmission
  2. Identity Verification: Ensures users connect to the genuine server, not a man-in-the-middle attack
  3. Trust Establishment: Builds user trust through trusted Certificate Authorities (CAs)
  4. Compliance Requirements: Meets various regulations and standards for data protection

Common challenges with manual certificate management:

  • Delayed expiration alerts: Easy to miss certificate renewal dates, leading to service outages
  • Low batch deployment efficiency: Manually configuring certificates on each server is time-consuming
  • Configuration inconsistency: Certificate configurations may differ across servers
  • High error risk: Manual operations are error-prone and may cause service unavailability

Ansible Automation Management Architecture

As a powerful automation operations tool, Ansible’s advantages include:

  • Agentless architecture: No client installation required on target nodes
  • Simple and easy to use: Uses YAML for configuration files, easy to understand and maintain
  • Modular design: Rich module library supporting various operations tasks
  • Idempotency: Safe to execute repeatedly, ensuring consistent system state

Basic Architecture Design

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.
├── files/                    # Certificate file storage directory
│   ├── domain1.com.crt      # Certificate file
│   ├── domain1.com.key      # Private key file
│   └── ...
├── handlers/                 # Handler files
│   └── main.yaml             # Service restart handler
├── tasks/                    # Task files
│   └── main.yml              # Main task definitions
├── templates/                # Template files
│   ├── nginx.conf.j2         # Nginx configuration template
│   └── ...
├── vars/                     # Variable files
│   └── main.yml              # Configuration variables
└── README.md                 # Project documentation

Complete Implementation Solution

1. Directory Structure Creation

First, create the basic directory structure:

bash
1
2
mkdir -p ssl-certs/ansible/{files,handlers,tasks,templates,vars}
cd ssl-certs/ansible

2. Variable Configuration File

Create the vars/main.yml file to define the basic certificate management configuration:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# coding: utf-8
---
# SSL certificate management configuration

# Client certificate configuration
ssl_certificates:
  "example-client":
    server_ip: ["203.0.113.10", "203.0.113.11"]
    ssl_cer_name: "mail.example.com"
    domain_name: "example.com"
    server_name: "mail.example.com"
    ssl_force: true
    ssl_redirect: true

  "another-client":
    server_ip: ["198.51.100.20", "198.51.100.21"]
    ssl_cer_name: "secure.example.org"
    domain_name: "example.org"
    server_name: "mail.example.org"
    ssl_force: false
    ssl_redirect: false

# Multi-domain certificate configuration
multi_domain_ssl:
  # Domains requiring forced HTTPS
  force_ssl_domain: [
    "secure.example.com",
    "api.example.com",
    "admin.example.com"
  ]
  
  # Standard HTTPS domains
  standard_ssl_domain: [
    "shop.example.com",
    "blog.example.com"
  ]

# SSL certificate configuration
ssl_config:
  ssl_certificate_path: "/etc/ssl/certs"
  ssl_private_key_path: "/etc/ssl/private"
  ssl_dhparam_path: "/etc/ssl/certs/dhparam.pem"
  ssl_session_timeout: "5m"
  ssl_protocols: "TLSv1.2 TLSv1.3"
  ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"

# Nginx configuration
nginx_config:
  server_port: 80
  ssl_server_port: 443
  worker_processes: "auto"
  worker_connections: 1024
  keepalive_timeout: "65"
  gzip: true

3. Task Definition File

Create the tasks/main.yml file to define the main certificate management tasks:

yaml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
---
- name: SSL certificate management tasks
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    # 1. Ensure certificate directories exist
    - name: Create certificate directories
      file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - "{{ ssl_config.ssl_certificate_path }}"
        - "{{ ssl_config.ssl_private_key_path }}"
        - "{{ ssl_config.ssl_dhparam_path | dirname }}"
    
    # 2. Copy certificate files
    - name: Copy SSL certificate files
      copy:
        src: "files/{{ item.ssl_cer_name }}.crt"
        dest: "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
        mode: '0644'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    # 3. Copy private key files
    - name: Copy SSL private key files
      copy:
        src: "files/{{ item.ssl_cer_name }}.key"
        dest: "{{ ssl_config.ssl_private_key_path }}/{{ item.ssl_cer_name }}.key"
        mode: '0600'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    # 4. Generate DH parameters
    - name: Generate DH parameters
      command: openssl dhparam -out {{ ssl_config.ssl_dhparam_path }} 2048
      args:
        creates: "{{ ssl_config.ssl_dhparam_path }}"
      notify: restart nginx
    
    # 5. Generate Nginx configuration
    - name: Generate Nginx configuration file
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        mode: '0644'
        owner: root
        group: root
      notify: restart nginx
    
    # 6. Generate site configuration
    - name: Generate site configuration files
      template:
        src: site_config.j2
        dest: "/etc/nginx/conf.d/{{ item.domain_name }}.conf"
        mode: '0644'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    # 7. Set SELinux (if enabled)
    - name: Set SELinux context
      sefcontext:
        target: "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.*"
        setype: httpd_sys_content_t
        recurse: yes
      loop: "{{ ssl_certificates.values() }}"
      when: ansible_selinux.status == 'enabled'
    
    # 8. Verify SSL certificates
    - name: Verify SSL certificates
      command: openssl x509 -noout -text -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_check
    
    # 9. Check certificate expiration
    - name: Check certificate expiration
      command: openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_expiry
    
    # 10. Output certificate information
    - name: Output certificate information
      debug:
        msg: "Certificate {{ item.ssl_cer_name }} expires: {{ item.stdout }}"
      loop: "{{ cert_expiry.results }}"
      loop_control:
        label: "{{ item.item.ssl_cer_name }}"

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
      listen: restart nginx

- name: SSL certificate report generation
  hosts: localhost
  gather_facts: no
  
  tasks:
    - name: Generate SSL certificate report
      template:
        src: ssl_report.j2
        dest: "/tmp/ssl_certificate_report.html"
      delegate_to: localhost

4. Nginx Configuration Template

Create the templates/nginx.conf.j2 file:

jinja2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
user nginx;
worker_processes {{ nginx_config.worker_processes }};
worker_rlimit_nofile 100000;

events {
    worker_connections {{ nginx_config.worker_connections }};
    multi_accept on;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Log format
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log;

    # Basic settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout {{ nginx_config.keepalive_timeout }};
    types_hash_max_size 2048;
    server_tokens off;

    # SSL settings
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout {{ ssl_config.ssl_session_timeout }};
    ssl_protocols {{ ssl_config.ssl_protocols }};
    ssl_ciphers '{{ ssl_config.ssl_ciphers }}';
    ssl_prefer_server_ciphers on;
    ssl_dhparam {{ ssl_config.ssl_dhparam_path }};

    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Gzip compression settings
    {{- if nginx_config.gzip }}
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied expired no-cache no-store private must-revalidate max-age-0 auth;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    {{- endif }}

    # Include site configurations
    include /etc/nginx/conf.d/*.conf;
}

5. Site Configuration Template

Create the templates/site_config.j2 file:

jinja2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
server {
    listen {{ nginx_config.server_port }};
    listen {{ nginx_config.ssl_server_port }} ssl;

    # Basic settings
    server_name {{ item.domain_name }};
    root /var/www/{{ item.domain_name }};
    index index.html index.htm;

    # SSL configuration
    ssl_certificate {{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt;
    ssl_certificate_key {{ ssl_config.ssl_private_key_path }}/{{ item.ssl_cer_name }}.key;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 5m;

    # Redirect configuration
    {{- if item.ssl_redirect and item.ssl_force }}
    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    }
    {{- endif }}

    # Log configuration
    access_log /var/log/nginx/{{ item.domain_name }}_access.log;
    error_log /var/log/nginx/{{ item.domain_name }}_error.log;

    # Web root directory permissions
    location / {
        try_files $uri $uri/ =404;
    }

    # Static file caching
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Security settings
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # Error pages
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

    location = /50x.html {
        root /usr/share/nginx/html;
    }

    # PHP support (if needed)
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

6. Handler Configuration

Create the handlers/main.yaml file:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
- name: restart nginx
  service:
    name: nginx
    state: restarted
    enabled: yes

- name: reload nginx
  service:
    name: nginx
    state: reloaded
    enabled: yes

- name: start nginx
  service:
    name: nginx
    state: started
    enabled: yes

- name: stop nginx
  service:
    name: nginx
    state: stopped

7. SSL Report Template

Create the templates/ssl_report.j2 file:

jinja2
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSL Certificate Management Report</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        .cert-card {
            border: 1px solid #ddd;
            border-radius: 8px;
            margin-bottom: 20px;
            overflow: hidden;
        }
        .cert-header {
            background-color: #007bff;
            color: white;
            padding: 15px;
            font-weight: bold;
        }
        .cert-body {
            padding: 15px;
        }
        .cert-info {
            margin-bottom: 10px;
        }
        .cert-info label {
            font-weight: bold;
            color: #555;
        }
        .status {
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: bold;
        }
        .status.valid {
            background-color: #d4edda;
            color: #155724;
        }
        .status.expired {
            background-color: #f8d7da;
            color: #721c24;
        }
        .status.expiring {
            background-color: #fff3cd;
            color: #856404;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 12px;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
            font-weight: bold;
        }
        .summary {
            background-color: #e9ecef;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>SSL Certificate Management Report</h1>
        
        <div class="summary">
            <h2>Report Summary</h2>
            <p>Generated: {{ ansible_date_time.date }} {{ ansible_date_time.time }}</p>
            <p>Total Certificates: {{ ssl_certificates | length }}</p>
            <p>Valid Certificates: {{ valid_certs }}</p>
            <p>Expired Certificates: {{ expired_certs }}</p>
            <p>Expiring Soon: {{ expiring_certs }}</p>
        </div>

        <h2>Certificate Details</h2>
        {% for cert_name, cert_info in ssl_certificates.items() %}
        <div class="cert-card">
            <div class="cert-header">
                {{ cert_info.domain_name }} - {{ cert_info.ssl_cer_name }}
            </div>
            <div class="cert-body">
                <div class="cert-info">
                    <label>Status:</label>
                    <span class="status {{ cert_info.status }}">{{ cert_info.status_text }}</span>
                </div>
                <div class="cert-info">
                    <label>IP Address:</label>
                    {{ cert_info.server_ip | join(', ') }}
                </div>
                <div class="cert-info">
                    <label>Domain:</label>
                    {{ cert_info.domain_name }}
                </div>
                <div class="cert-info">
                    <label>Server Name:</label>
                    {{ cert_info.server_name }}
                </div>
                <div class="cert-info">
                    <label>Force HTTPS:</label>
                    {{ cert_info.ssl_force | string | lower }}
                </div>
                <div class="cert-info">
                    <label>Expiration Date:</label>
                    {{ cert_info.expiry_date }}
                </div>
                <div class="cert-info">
                    <label>Days Remaining:</label>
                    {{ cert_info.days_remaining }}
                </div>
            </div>
        </div>
        {% endfor %}

        <h2>Configuration Details</h2>
        <table>
            <thead>
                <tr>
                    <th>Setting</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Certificate Path</td>
                    <td>{{ ssl_config.ssl_certificate_path }}</td>
                </tr>
                <tr>
                    <td>Private Key Path</td>
                    <td>{{ ssl_config.ssl_private_key_path }}</td>
                </tr>
                <tr>
                    <td>DH Parameters Path</td>
                    <td>{{ ssl_config.ssl_dhparam_path }}</td>
                </tr>
                <tr>
                    <td>SSL Protocols</td>
                    <td>{{ ssl_config.ssl_protocols }}</td>
                </tr>
                <tr>
                    <td>Cipher Suites</td>
                    <td>{{ ssl_config.ssl_ciphers }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

Certificate Verification and Checking

Local Certificate Content Verification

Before deploying certificates, it’s recommended to verify their content locally:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# View certificate details
openssl x509 -noout -text -in mail.example.com.crt

# Verify certificate format
openssl x509 -in mail.example.com.crt -text -noout | grep "Subject:"

# Verify certificate validity period
openssl x509 -in mail.example.com.crt -noout -dates

# Verify certificate chain integrity
openssl verify -CAfile ca-bundle.crt mail.example.com.crt

# Check certificate private key match
openssl pkey -in mail.example.com.key -pubout | openssl dgst -sha256 -verify <(openssl x509 -in mail.example.com.crt -pubkey -noout) -signature <(openssl x509 -in mail.example.com.crt -noout -signer mail.example.com.crt)

Server Connection Certificate Verification

After deployment, verify certificates are working properly:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Connect directly to SSL port for verification
openssl s_client -connect mail.example.com:443

# Verify certificate chain
openssl s_client -connect mail.example.com:443 -showcerts

# Check SSL/TLS protocol support
openssl s_client -connect mail.example.com:443 -tls1_2

# Test SSL handshake performance
openssl s_client -connect mail.example.com:443 -msg

Ansible Playbook Usage Examples

Basic Deployment Playbook

Create the deploy_ssl.yml file:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
---
- name: SSL certificate batch deployment
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: Create certificate directories
      file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - "{{ ssl_config.ssl_certificate_path }}"
        - "{{ ssl_config.ssl_private_key_path }}"
    
    - name: Copy certificate files
      copy:
        src: "files/{{ item.ssl_cer_name }}.crt"
        dest: "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
        mode: '0644'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    - name: Copy private key files
      copy:
        src: "files/{{ item.ssl_cer_name }}.key"
        dest: "{{ ssl_config.ssl_private_key_path }}/{{ item.ssl_cer_name }}.key"
        mode: '0600'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    - name: Generate site configuration
      template:
        src: site_config.j2
        dest: "/etc/nginx/conf.d/{{ item.domain_name }}.conf"
        mode: '0644'
        owner: root
        group: root
      loop: "{{ ssl_certificates.values() }}"
      notify: restart nginx
    
    - name: Verify SSL configuration
      command: nginx -t
      notify: restart nginx

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

Certificate Check Playbook

Create the check_ssl.yml file:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
---
- name: SSL certificate check
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: Check certificate file existence
      stat:
        path: "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      register: cert_stat
      loop: "{{ ssl_certificates.values() }}"
    
    - name: Check certificate validity period
      command: openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_expiry
      when: item.stat.exists is defined and item.stat.exists
    
    - name: Calculate remaining days
      command: >
        openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt" 
        | awk -F= '/notAfter/ {print $2; system("date -d \""$2\" +%s")}' 
        | tail -1 | xargs -I {} expr $(( ({} - $(date +%s)) / 86400 ))
      loop: "{{ ssl_certificates.values() }}"
      register: days_remaining
      when: item.stat.exists is defined and item.stat.exists
    
    - name: Generate certificate report
      template:
        src: ssl_report.j2
        dest: "/tmp/ssl_certificate_report_{{ item.domain_name }}.html"
      loop: "{{ ssl_certificates.values() }}"
      when: item.stat.exists is defined and item.stat.exists

Automated Deployment Workflow

1. Certificate Preparation Phase

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Generate private key
openssl genrsa -out mail.example.com.key 2048

# Generate Certificate Signing Request (CSR)
openssl req -new -key mail.example.com.key -out mail.example.com.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=Example Company/CN=mail.example.com"

# Sign certificate with CA
openssl x509 -req -in mail.example.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out mail.example.com.crt -days 365

# Or use Let's Encrypt to obtain a certificate
certbot certonly --standalone -d mail.example.com --email [email protected] --agree-tos

2. Ansible Inventory Configuration

Create the inventory file:

ini
1
2
3
4
5
6
7
[ssl_servers]
web-server-1 ansible_host=203.0.113.10
web-server-2 ansible_host=203.0.113.11

[ssl_servers:vars]
ansible_ssh_user=admin
ansible_ssh_private_key_file=~/.ssh/id_rsa

3. Execute Deployment

bash
1
2
3
4
5
6
7
8
# Execute SSL certificate deployment
ansible-playbook -i inventory deploy_ssl.yml

# Execute certificate check
ansible-playbook -i inventory check_ssl.yml

# Check certificates on specific servers only
ansible-playbook -i inventory --limit web-server-1 check_ssl.yml

Monitoring and Alerting

Certificate Expiration Monitoring

Create the monitor_ssl_expiry.yml file:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
---
- name: Monitor SSL certificate expiration
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: Get certificate expiration dates
      command: openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_expiry
    
    - name: Check if certificates are expiring soon
      debug:
        msg: "Certificate {{ item.item.ssl_cer_name }} is expiring on {{ item.stdout }}, please renew promptly!"
      loop: "{{ cert_expiry.results }}"
      loop_control:
        label: "{{ item.item.ssl_cer_name }}"
      when: >
        (ansible_date_time.epoch | int + 2592000) > (item.stdout | regex_findall('notAfter=(.+)') | first | string | to_datetime('%b %d %H:%M:%S %Y %Z') | timestamp)

Auto-Renewal Script

Create the auto_renew_ssl.sh script:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/bin/bash

# SSL certificate auto-renewal script
LOG_FILE="/var/log/ssl_renewal.log"
NOTIFICATION_EMAIL="[email protected]"

log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

check_certificate_expiry() {
    local cert_file=$1
    local domain_name=$2
    local expiry_days=$(openssl x509 -enddate -noout -in "$cert_file" | awk -F= '/notAfter/ {print $2; system("date -d \""$2\" +%s")}' | tail -1 | xargs -I {} expr $(( ({} - $(date +%s)) / 86400 ))
    
    echo "$expiry_days"
}

renew_certificate() {
    local domain_name=$1
    log_message "Starting certificate renewal: $domain_name"
    
    # Renew using Let's Encrypt
    certbot renew --cert-name "$domain_name" --quiet
    
    if [ $? -eq 0 ]; then
        log_message "Certificate $domain_name renewed successfully"
        # Restart nginx to load new certificate
        systemctl restart nginx
        log_message "Nginx service restarted"
    else
        log_message "Certificate $domain_name renewal failed"
        # Send alert email
        echo "Certificate $domain_name renewal failed, please check immediately" | mail -s "SSL Certificate Renewal Failure Alert" "$NOTIFICATION_EMAIL"
    fi
}

# Check all certificates
for cert in /etc/letsencrypt/live/*/cert.pem; do
    if [ -f "$cert" ]; then
        domain_name=$(basename $(dirname "$cert"))
        expiry_days=$(check_certificate_expiry "$cert" "$domain_name")
        
        log_message "Domain: $domain_name, Days remaining: $expiry_days"
        
        # Renew if less than 30 days remaining
        if [ "$expiry_days" -lt 30 ]; then
            renew_certificate "$domain_name"
        fi
    fi
done

log_message "SSL certificate renewal check completed"

Security Best Practices

1. Certificate Security Management

  • Private key protection: Ensure private key file permissions are 600, accessible only by root
  • Regular rotation: Renew certificates every 90-180 days
  • Backup strategy: Regularly back up certificate and private key files
  • Access control: Restrict access to certificate files

2. Configuration Security

  • Secure protocols: Enable only TLS 1.2 and TLS 1.3
  • Strong cipher suites: Use strong encryption suites
  • HTTP Strict Transport Security: Enable HSTS headers
  • Security headers: Add appropriate security HTTP headers

3. Monitoring and Alerting

  • Expiration monitoring: Monitor remaining certificate validity period
  • Vulnerability scanning: Regularly scan SSL/TLS configuration vulnerabilities
  • Performance monitoring: Monitor SSL/TLS handshake performance
  • Access logging: Monitor abnormal access patterns

Troubleshooting

Common Issues and Solutions

1. Website Inaccessible After Certificate Installation

bash
1
2
3
4
5
6
7
8
9
# Check nginx configuration syntax
nginx -t

# Check certificate file permissions
ls -la /etc/ssl/certs/
ls -la /etc/ssl/private/

# Check if certificate is loaded correctly
openssl s_client -connect mail.example.com:443

2. Incomplete Certificate Chain

bash
1
2
3
4
5
6
# Check certificate chain
openssl s_client -connect mail.example.com:443 -showcerts

# Manually build certificate chain
cat intermediate.crt > fullchain.crt
cat your_domain.crt >> fullchain.crt

3. SSL/TLS Handshake Failure

bash
1
2
3
4
5
# Check protocol support
openssl s_client -connect mail.example.com:443 -tls1_2

# Check cipher suite support
openssl s_client -connect mail.example.com:443 -cipher ECDHE-RSA-AES128-GCM-SHA256

4. Incorrect Certificate Expiration Calculation

bash
1
2
# Calculate expiration precisely
openssl x509 -enddate -noout -in mail.example.com.crt | awk -F= '/notAfter/ {print "Expiration:", $2; print "Days remaining:", int((mktime(gensub(/(.+) (.+) (.+) (.+) (.+) (.+)/,"\\1 \\2 \\3 \\4 \\5 \\6","g", $2))-mktime(gensub(/(.+) (.+) (.+) (.+) (.+) (.+)/,"\\1 \\2 \\3 \\4 \\5 \\6","g","now")))/86400)}'

Log Analysis

bash
1
2
3
4
5
6
7
8
# View nginx error logs
tail -f /var/log/nginx/error.log

# View SSL errors
grep "SSL" /var/log/nginx/error.log

# Analyze SSL-related errors in access logs
grep "443" /var/log/nginx/access.log | grep -E "400|403|502"

Summary

Through this detailed guide, we’ve learned how to use Ansible for automated batch deployment and renewal management of SSL certificates. This solution offers the following advantages:

Key Benefits:

  1. High automation: Reduces manual operations and minimizes human error
  2. Consistency: Ensures uniform configuration across all servers
  3. Scalability: Easy to add new certificates and servers
  4. Comprehensive monitoring: Provides full certificate status monitoring
  5. Rapid response: Automated renewal and alerting mechanisms

Implementation Recommendations:

  1. Phased rollout: Verify in test environments first, then gradually promote to production
  2. Regular maintenance: Periodically review configurations and update best practices
  3. Documentation: Keep related documentation and operation manuals up to date
  4. Team training: Train relevant personnel to ensure correct usage
  5. Backup strategy: Establish comprehensive backup and recovery mechanisms

Future Directions:

  1. CI/CD integration: Integrate SSL certificate management into automated deployment workflows
  2. Cloud platform integration: Support cloud platform certificate management services
  3. Intelligent monitoring: Use AI technology for smarter monitoring and prediction
  4. Multi-platform support: Extend support to other web servers and application servers
  5. Toolchain integration: Integrate with other operations tools for more comprehensive management

SSL certificate automation management is an important part of modern operations. By properly using automation tools like Ansible, you can significantly improve operations efficiency, reduce security risks, and ensure stable operation of enterprise services. We hope the solution provided in this article offers valuable reference for your practical work.