SSL证书自动化管理——Ansible批量部署与更新实践

在现代化的 IT 基础设施管理中,SSL证书的安全管理变得越来越重要。随着服务数量的增长和证书更新周期的缩短,手动管理证书变得效率低下且容易出错。本文将详细介绍如何使用 Ansible 实现证书的自动化批量部署与更新,提高管理效率,降低安全风险。

SSL证书管理的重要性

SSL证书是网络通信安全的基础,它确保了:

  1. 数据加密保护: 防止敏感数据在传输过程中被窃听
  2. 身份验证: 确保用户连接到真正的服务器,而不是中间人攻击
  3. 信任建立: 通过受信任的证书颁发机构(CA)建立用户信任
  4. 合规要求: 满足各种法规和标准对数据保护的要求

手动管理证书的常见挑战:

  • 证书过期提醒不及时: 容易错过证书更新时间,导致服务中断
  • 批量部署效率低: 手动为每个服务器配置证书耗费大量时间
  • 配置不一致: 不同服务器的证书配置可能存在差异
  • 错误风险高: 手动操作容易出错,可能导致服务不可用

Ansible 自动化管理架构

Ansible 作为一款强大的自动化运维工具,其优势在于:

  • 无代理架构: 不需要在目标节点上安装客户端
  • 简单易用: 使用 YAML 语言编写配置文件,易于理解和维护
  • 模块化设计: 丰富的模块库支持各种运维任务
  • 幂等性: 可以安全地重复执行,确保系统状态一致

基本架构设计

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.
├── files/                    # 证书文件存储目录
│   ├── domain1.com.crt      # 证书文件
│   ├── domain1.com.key      # 私钥文件
│   └── ...
├── handlers/                 # 处理器文件
│   └── main.yaml             # 服务重启处理器
├── tasks/                    # 任务文件
│   └── main.yml              # 主要任务定义
├── templates/                # 模板文件
│   ├── nginx.conf.j2         # Nginx 配置模板
│   └── ...
├── vars/                     # 变量文件
│   └── main.yml              # 配置变量
└── README.md                 # 项目说明

完整实现方案

1. 目录结构创建

首先创建基础的目录结构:

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

2. 变量配置文件

创建 vars/main.yml 文件,定义证书管理的基本配置:

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证书管理配置

# 客户端证书配置
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_ssl:
  # 强制HTTPS的域名列表
  force_ssl_domain: [
    "secure.example.com",
    "api.example.com",
    "admin.example.com"
  ]
  
  # 普通HTTPS域名列表
  standard_ssl_domain: [
    "shop.example.com",
    "blog.example.com"
  ]

# SSL证书配置
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配置
nginx_config:
  server_port: 80
  ssl_server_port: 443
  worker_processes: "auto"
  worker_connections: 1024
  keepalive_timeout: "65"
  gzip: true

3. 任务定义文件

创建 tasks/main.yml 文件,定义证书管理的主要任务:

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证书管理任务
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    # 1. 确保证书目录存在
    - name: 创建证书目录
      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. 复制证书文件
    - name: 复制SSL证书文件
      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. 复制私钥文件
    - name: 复制SSL私钥文件
      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. 生成DH参数
    - name: 生成DH参数
      command: openssl dhparam -out {{ ssl_config.ssl_dhparam_path }} 2048
      args:
        creates: "{{ ssl_config.ssl_dhparam_path }}"
      notify: restart nginx
    
    # 5. 生成Nginx配置
    - name: 生成Nginx配置文件
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        mode: '0644'
        owner: root
        group: root
      notify: restart nginx
    
    # 6. 生成站点配置
    - name: 生成站点配置文件
      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. 设置SELinux(如果启用)
    - name: 设置SELinux上下文
      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. 验证SSL证书
    - name: 验证SSL证书
      command: openssl x509 -noout -text -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_check
    
    # 9. 检查证书过期时间
    - name: 检查证书过期时间
      command: openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_expiry
    
    # 10. 输出证书信息
    - name: 输出证书信息
      debug:
        msg: "证书 {{ item.ssl_cer_name }} 过期时间: {{ 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证书报告生成
  hosts: localhost
  gather_facts: no
  
  tasks:
    - name: 生成SSL证书报告
      template:
        src: ssl_report.j2
        dest: "/tmp/ssl_certificate_report.html"
      delegate_to: localhost

4. Nginx 配置模板

创建 templates/nginx.conf.j2 文件:

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 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;

    # 基础设置
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout {{ nginx_config.keepalive_timeout }};
    types_hash_max_size 2048;
    server_tokens off;

    # SSL设置
    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 }};

    # 安全头设置
    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压缩设置
    {{- 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 /etc/nginx/conf.d/*.conf;
}

5. 站点配置模板

创建 templates/site_config.j2 文件:

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;

    # 基本设置
    server_name {{ item.domain_name }};
    root /var/www/{{ item.domain_name }};
    index index.html index.htm;

    # SSL配置
    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;

    # 重定向配置
    {{- if item.ssl_redirect and item.ssl_force }}
    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    }
    {{- endif }}

    # 日志配置
    access_log /var/log/nginx/{{ item.domain_name }}_access.log;
    error_log /var/log/nginx/{{ item.domain_name }}_error.log;

    # 网站根目录权限
    location / {
        try_files $uri $uri/ =404;
    }

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

    # 安全设置
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    # 错误页面
    error_page 404 /404.html;
    error_page 500 502 503 504 /50x.html;

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

    # PHP支持(如果需要)
    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. 处理器配置

创建 handlers/main.yaml 文件:

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报告模板

创建 templates/ssl_report.j2 文件:

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="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSL证书管理报告</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证书管理报告</h1>
        
        <div class="summary">
            <h2>报告摘要</h2>
            <p>生成时间: {{ ansible_date_time.date }} {{ ansible_date_time.time }}</p>
            <p>总证书数: {{ ssl_certificates | length }}</p>
            <p>有效证书数: {{ valid_certs }}</p>
            <p>过期证书数: {{ expired_certs }}</p>
            <p>即将过期证书数: {{ expiring_certs }}</p>
        </div>

        <h2>证书详细信息</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>状态:</label>
                    <span class="status {{ cert_info.status }}">{{ cert_info.status_text }}</span>
                </div>
                <div class="cert-info">
                    <label>IP地址:</label>
                    {{ cert_info.server_ip | join(', ') }}
                </div>
                <div class="cert-info">
                    <label>域名:</label>
                    {{ cert_info.domain_name }}
                </div>
                <div class="cert-info">
                    <label>服务器名:</label>
                    {{ cert_info.server_name }}
                </div>
                <div class="cert-info">
                    <label>强制HTTPS:</label>
                    {{ cert_info.ssl_force | string | lower }}
                </div>
                <div class="cert-info">
                    <label>过期时间:</label>
                    {{ cert_info.expiry_date }}
                </div>
                <div class="cert-info">
                    <label>剩余天数:</label>
                    {{ cert_info.days_remaining }}
                </div>
            </div>
        </div>
        {% endfor %}

        <h2>配置详情</h2>
        <table>
            <thead>
                <tr>
                    <th>配置项</th>
                    <th>值</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>证书路径</td>
                    <td>{{ ssl_config.ssl_certificate_path }}</td>
                </tr>
                <tr>
                    <td>私钥路径</td>
                    <td>{{ ssl_config.ssl_private_key_path }}</td>
                </tr>
                <tr>
                    <td>DH参数路径</td>
                    <td>{{ ssl_config.ssl_dhparam_path }}</td>
                </tr>
                <tr>
                    <td>SSL协议</td>
                    <td>{{ ssl_config.ssl_protocols }}</td>
                </tr>
                <tr>
                    <td>加密套件</td>
                    <td>{{ ssl_config.ssl_ciphers }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

证书验证和检查

本地验证证书内容

在部署证书之前,建议先在本地验证证书的内容:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 查看证书详细信息
openssl x509 -noout -text -in mail.example.com.crt

# 验证证书格式
openssl x509 -in mail.example.com.crt -text -noout | grep "Subject:"

# 验证证书有效期
openssl x509 -in mail.example.com.crt -noout -dates

# 验证证书链完整性
openssl verify -CAfile ca-bundle.crt mail.example.com.crt

# 检查证书私钥匹配
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)

连接服务器验证证书

部署完成后,可以通过以下方式验证证书是否正常工作:

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 直接连接SSL端口验证
openssl s_client -connect mail.example.com:443

# 验证证书链
openssl s_client -connect mail.example.com:443 -showcerts

# 检查SSL/TLS协议支持
openssl s_client -connect mail.example.com:443 -tls1_2

# 测试SSL握手性能
openssl s_client -connect mail.example.com:443 -msg

Ansible Playbook 使用示例

基本部署 Playbook

创建 deploy_ssl.yml 文件:

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证书批量部署
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: 创建证书目录
      file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - "{{ ssl_config.ssl_certificate_path }}"
        - "{{ ssl_config.ssl_private_key_path }}"
    
    - name: 复制证书文件
      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:
        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: 生成站点配置
      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: 验证SSL配置
      command: nginx -t
      notify: restart nginx

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

证书检查 Playbook

创建 check_ssl.yml 文件:

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证书检查
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: 检查证书文件存在性
      stat:
        path: "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      register: cert_stat
      loop: "{{ ssl_certificates.values() }}"
    
    - name: 检查证书有效期
      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: 计算剩余天数
      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: 生成证书报告
      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

自动化部署流程

1. 证书准备阶段

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 生成私钥
openssl genrsa -out mail.example.com.key 2048

# 生成证书签名请求(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"

# 使用CA签发证书
openssl x509 -req -in mail.example.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out mail.example.com.crt -days 365

# 或者使用Let's Encrypt获取证书
certbot certonly --standalone -d mail.example.com --email [email protected] --agree-tos

2. Ansible Inventory配置

创建 inventory 文件:

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. 执行部署

bash
1
2
3
4
5
6
7
8
# 执行SSL证书部署
ansible-playbook -i inventory deploy_ssl.yml

# 执行证书检查
ansible-playbook -i inventory check_ssl.yml

# 只检查特定服务器的证书
ansible-playbook -i inventory --limit web-server-1 check_ssl.yml

监控和告警

证书过期监控

创建 monitor_ssl_expiry.yml 文件:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
---
- name: 监控SSL证书过期
  hosts: ssl_servers
  become: yes
  vars_files:
    - vars/main.yml
  
  tasks:
    - name: 获取证书过期时间
      command: openssl x509 -enddate -noout -in "{{ ssl_config.ssl_certificate_path }}/{{ item.ssl_cer_name }}.crt"
      loop: "{{ ssl_certificates.values() }}"
      register: cert_expiry
    
    - name: 检查证书是否即将过期
      debug:
        msg: "证书 {{ item.item.ssl_cer_name }} 即将在 {{ item.stdout }} 过期,请及时更新!"
      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_renew_ssl.sh 脚本:

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证书自动续期脚本
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 "开始续期证书: $domain_name"
    
    # 使用Let's Encrypt续期
    certbot renew --cert-name "$domain_name" --quiet
    
    if [ $? -eq 0 ]; then
        log_message "证书 $domain_name 续期成功"
        # 重启nginx以加载新证书
        systemctl restart nginx
        log_message "已重启nginx服务"
    else
        log_message "证书 $domain_name 续期失败"
        # 发送告警邮件
        echo "证书 $domain_name 续期失败,请立即检查" | mail -s "SSL证书续期失败告警" "$NOTIFICATION_EMAIL"
    fi
}

# 检查所有证书
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_name, 剩余天数: $expiry_days"
        
        # 如果剩余天数少于30天,进行续期
        if [ "$expiry_days" -lt 30 ]; then
            renew_certificate "$domain_name"
        fi
    fi
done

log_message "SSL证书续期检查完成"

安全最佳实践

1. 证书安全管理

  • 私钥保护: 确保私钥文件权限为 600,只有root用户可访问
  • 定期轮换: 建议每90-180天更新一次证书
  • 备份策略: 定期备份证书和私钥文件
  • 访问控制: 限制证书文件的访问权限

2. 配置安全

  • 安全协议: 只启用 TLS 1.2 和 TLS 1.3
  • 强密码套件: 使用强加密套件
  • HTTP严格传输安全: 启用 HSTS 头
  • 安全头: 添加适当的安全HTTP头

3. 监控告警

  • 过期监控: 监控证书剩余有效期
  • 漏洞扫描: 定期扫描SSL/TLS配置漏洞
  • 性能监控: 监控SSL/TLS握手性能
  • 访问日志: 监控异常访问模式

故障排除

常见问题及解决方案

1. 证书安装后网站无法访问

bash
1
2
3
4
5
6
7
8
9
# 检查nginx配置语法
nginx -t

# 检查证书文件权限
ls -la /etc/ssl/certs/
ls -la /etc/ssl/private/

# 检查证书是否正确加载
openssl s_client -connect mail.example.com:443

2. 证书链不完整

bash
1
2
3
4
5
6
# 检查证书链
openssl s_client -connect mail.example.com:443 -showcerts

# 手动构建证书链
cat intermediate.crt > fullchain.crt
cat your_domain.crt >> fullchain.crt

3. SSL/TLS握手失败

bash
1
2
3
4
5
# 检查协议支持
openssl s_client -connect mail.example.com:443 -tls1_2

# 检查加密套件支持
openssl s_client -connect mail.example.com:443 -cipher ECDHE-RSA-AES128-GCM-SHA256

4. 证书过期时间计算错误

bash
1
2
# 精确计算过期时间
openssl x509 -enddate -noout -in mail.example.com.crt | awk -F= '/notAfter/ {print "过期时间:", $2; print "剩余天数:", int((mktime(gensub(/(.+) (.+) (.+) (.+) (.+) (.+)/,"\\1 \\2 \\3 \\4 \\5 \\6","g", $2))-mktime(gensub(/(.+) (.+) (.+) (.+) (.+) (.+)/,"\\1 \\2 \\3 \\4 \\5 \\6","g","now")))/86400)}'

日志分析

bash
1
2
3
4
5
6
7
8
# 查看nginx错误日志
tail -f /var/log/nginx/error.log

# 查看SSL错误
grep "SSL" /var/log/nginx/error.log

# 分析访问日志中的SSL相关错误
grep "443" /var/log/nginx/access.log | grep -E "400|403|502"

总结

通过本文的详细介绍,我们了解了如何使用 Ansible 实现 SSL 证书的自动化批量部署与更新管理。这套解决方案具有以下优势:

主要优势:

  1. 自动化程度高: 减少手动操作,降低人为错误
  2. 一致性保证: 确保所有服务器配置统一
  3. 可扩展性强: 容易添加新的证书和服务器
  4. 监控完善: 提供全面的证书状态监控
  5. 快速响应: 自动化续期和告警机制

实施建议:

  1. 分阶段实施: 先在测试环境验证,再逐步推广到生产环境
  2. 定期维护: 定期检查配置和更新最佳实践
  3. 文档更新: 及时更新相关文档和操作手册
  4. 团队培训: 对相关人员进行培训,确保正确使用
  5. 备份策略: 建立完善的备份和恢复机制

未来发展方向:

  1. 集成到CI/CD: 将SSL证书管理集成到自动化部署流程中
  2. 云平台集成: 支持云平台的证书管理服务
  3. 智能化监控: 使用AI技术实现更智能的监控和预测
  4. 多平台支持: 扩展支持其他Web服务器和应用服务器
  5. 集成自动化: 与其他运维工具集成,实现更全面的管理

SSL证书自动化管理是现代化运维的重要组成部分,通过合理使用 Ansible 等自动化工具,可以显著提高运维效率,降低安全风险,确保企业服务的稳定运行。希望本文提供的方案能够为你的实际工作提供有价值的参考。