假設某家公司的系統管理員每次新建伺服器時,都需要手動執行一份「安全檢查清單」來關閉不必要的服務、設定防火牆規則、調整 SSH 設定等等...
這看起來很完美對吧?
但現實是:
這就是為什麼我們需要 Security as Code,讓安全配置像程式碼一樣可重複、可驗證、可版控。
💡Tips:資訊安全從來不是靠人的記憶力,而是具有規範的執行,但由於手動執行絕對會有誤差,所以我們需要導入自動化,每一次的操作都是一樣且具可追朔性的。
經過這幾年的實務經驗,筆者認為 Ansible 在安全自動化領域有幾個獨特優勢:
當然,市面上也有其他優秀的工具如 Chef、Puppet,但 Ansible 的學習曲線相對平緩,非常適合 DevOps 團隊快速上手。
CIS (Center for Internet Security) Benchmark 是業界公認的安全配置基準,涵蓋作業系統、資料庫、網頁伺服器等各種系統的安全設定建議。
以 Ubuntu 為例,CIS Benchmark 包含了 200+ 項安全檢查項目,分為:
讓我們來看看如何用 Ansible 實現這些安全基線!
首先建立一個專門的安全 Role 結構:
mkdir -p roles/security_hardening/{tasks,handlers,templates,defaults,files}
---
# roles/security_hardening/defaults/main.yml
# SSH 安全設定
ssh_port: 22
ssh_permit_root_login: false
ssh_password_authentication: false
ssh_max_auth_tries: 3
ssh_client_alive_interval: 300
ssh_client_alive_count_max: 2
# 防火牆設定
firewall_enabled: true
firewall_default_policy: "DROP"
firewall_allowed_ports:
- "{{ ssh_port }}/tcp"
- "80/tcp"
- "443/tcp"
# 用戶和權限設定
admin_users:
- name: "deploy"
groups: ["sudo", "adm"]
shell: "/bin/bash"
- name: "monitoring"
groups: ["adm"]
shell: "/bin/bash"
# 系統安全參數
kernel_parameters:
# 網路安全
- name: "net.ipv4.ip_forward"
value: "0"
- name: "net.ipv4.conf.all.send_redirects"
value: "0"
- name: "net.ipv4.conf.default.send_redirects"
value: "0"
- name: "net.ipv4.conf.all.accept_redirects"
value: "0"
- name: "net.ipv4.conf.default.accept_redirects"
value: "0"
# 記憶體保護
- name: "kernel.dmesg_restrict"
value: "1"
- name: "kernel.kptr_restrict"
value: "2"
# 日誌和監控
log_retention_days: 90
audit_enabled: true
fail2ban_enabled: true
# 合規檢查
cis_level: "1" # 1 或 2
generate_compliance_report: true
---
# roles/security_hardening/tasks/main.yml
- name: 載入 OS 特定變數
include_vars: "{{ ansible_distribution }}.yml"
ignore_errors: yes
- name: 系統更新和基本安全
include_tasks: system_updates.yml
tags: ['system', 'updates']
- name: SSH 服務安全加固
include_tasks: ssh_hardening.yml
tags: ['ssh', 'network']
- name: 防火牆配置
include_tasks: firewall.yml
tags: ['firewall', 'network']
- name: 用戶和權限管理
include_tasks: user_management.yml
tags: ['users', 'permissions']
- name: 核心參數調整
include_tasks: kernel_hardening.yml
tags: ['kernel', 'system']
- name: 服務安全配置
include_tasks: service_hardening.yml
tags: ['services']
- name: 日誌和監控設定
include_tasks: logging_monitoring.yml
tags: ['logging', 'monitoring']
- name: 安全掃描和合規檢查
include_tasks: compliance_check.yml
tags: ['compliance', 'audit']
when: generate_compliance_report | bool
---
# roles/security_hardening/tasks/ssh_hardening.yml
- name: 備份原始 SSH 配置
copy:
src: /etc/ssh/sshd_config
dest: /etc/ssh/sshd_config.backup.{{ ansible_date_time.epoch }}
remote_src: yes
backup: yes
tags: ['backup']
- name: 生成安全的 SSH 配置
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0644'
backup: yes
# 在套用前先驗證配置語法
validate: '/usr/sbin/sshd -t -f %s'
notify: restart sshd
tags: ['ssh_config']
- name: 生成更安全的 SSH 主機金鑰
block:
- name: 移除弱加密的主機金鑰
file:
path: "/etc/ssh/{{ item }}"
state: absent
loop:
- ssh_host_dsa_key
- ssh_host_dsa_key.pub
- ssh_host_ecdsa_key
- ssh_host_ecdsa_key.pub
notify: restart sshd
- name: 生成強加密的 RSA 金鑰 (4096 bits)
command: ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N '' -q
args:
creates: /etc/ssh/ssh_host_rsa_key
- name: 生成 Ed25519 金鑰 (更安全的橢圓曲線)
command: ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N '' -q
args:
creates: /etc/ssh/ssh_host_ed25519_key
- name: 設定主機金鑰檔案權限
file:
path: "/etc/ssh/{{ item.name }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- { name: 'ssh_host_rsa_key', mode: '0600' }
- { name: 'ssh_host_rsa_key.pub', mode: '0644' }
- { name: 'ssh_host_ed25519_key', mode: '0600' }
- { name: 'ssh_host_ed25519_key.pub', mode: '0644' }
- name: 配置 SSH 客戶端安全設定
template:
src: ssh_config.j2
dest: /etc/ssh/ssh_config
owner: root
group: root
mode: '0644'
tags: ['ssh_client']
- name: 設定 SSH Banner (安全警告)
template:
src: ssh_banner.j2
dest: /etc/ssh/banner
owner: root
group: root
mode: '0644'
notify: restart sshd
when: ssh_banner_enabled | default(true)
# roles/security_hardening/templates/sshd_config.j2
# Managed by Ansible – DO NOT EDIT
# CIS Benchmark SSH 安全配置
# 基本設定
Port {{ ssh_port }}
Protocol 2
ListenAddress 0.0.0.0
# 主機金鑰 (只使用強加密演算法)
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
# 加密和演算法設定 (移除弱加密)
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512
# 認證設定
LoginGraceTime 30
PermitRootLogin {{ 'yes' if ssh_permit_root_login else 'no' }}
StrictModes yes
MaxAuthTries {{ ssh_max_auth_tries }}
MaxSessions 10
MaxStartups 10:30:100
# 密碼和金鑰認證
PasswordAuthentication {{ 'yes' if ssh_password_authentication else 'no' }}
PermitEmptyPasswords no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# 連線保持設定 (防止殭屍連線)
ClientAliveInterval {{ ssh_client_alive_interval }}
ClientAliveCountMax {{ ssh_client_alive_count_max }}
TCPKeepAlive yes
# 安全功能
UsePrivilegeSeparation yes
Compression delayed
UseDNS no
PermitUserEnvironment no
PermitTunnel no
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PrintMotd no
# 登入限制
{% if ssh_allowed_users is defined %}
AllowUsers {{ ssh_allowed_users | join(' ') }}
{% endif %}
{% if ssh_allowed_groups is defined %}
AllowGroups {{ ssh_allowed_groups | join(' ') }}
{% endif %}
{% if ssh_denied_users is defined %}
DenyUsers {{ ssh_denied_users | join(' ') }}
{% endif %}
# Banner 和日誌
{% if ssh_banner_enabled | default(true) %}
Banner /etc/ssh/banner
{% endif %}
SyslogFacility AUTHPRIV
LogLevel VERBOSE
# Subsystem
Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO
---
# roles/security_hardening/tasks/firewall.yml
- name: 安裝防火牆管理工具
package:
name: "{{ firewall_packages[ansible_os_family] | default(['ufw']) }}"
state: present
- name: UFW 防火牆配置 (Ubuntu/Debian)
block:
- name: 重置 UFW 防火牆規則
ufw:
state: reset
when: firewall_reset | default(false)
- name: 設定 UFW 預設政策
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: 'incoming', policy: 'deny' }
- { direction: 'outgoing', policy: 'allow' }
- { direction: 'routed', policy: 'deny' }
- name: 允許必要的服務端口
ufw:
rule: allow
port: "{{ item.split('/')[0] }}"
proto: "{{ item.split('/')[1] }}"
comment: "Ansible managed - {{ item }}"
loop: "{{ firewall_allowed_ports }}"
- name: 設定速率限制 (防 DDoS)
ufw:
rule: limit
port: "{{ ssh_port }}"
proto: tcp
comment: "SSH brute force protection"
- name: 允許內部網路通訊
ufw:
rule: allow
src: "{{ item }}"
comment: "Internal network"
loop: "{{ firewall_internal_networks | default([]) }}"
when: firewall_internal_networks is defined
- name: 啟用 UFW 防火牆
ufw:
state: enabled
logging: "{{ firewall_logging | default('on') }}"
when: ansible_os_family == "Debian" and firewall_enabled | bool
- name: 設定防火牆日誌輪轉
template:
src: firewall_logrotate.j2
dest: /etc/logrotate.d/ufw
owner: root
group: root
mode: '0644'
when: firewall_enabled | bool
---
# roles/security_hardening/tasks/kernel_hardening.yml
- name: 套用核心安全參數
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
sysctl_file: /etc/sysctl.d/99-security-hardening.conf
reload: yes
loop: "{{ kernel_parameters }}"
tags: ['sysctl']
- name: 設定額外的安全參數 (CIS Level 2)
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
sysctl_file: /etc/sysctl.d/99-security-hardening.conf
reload: yes
loop:
# ASLR (Address Space Layout Randomization)
- name: "kernel.randomize_va_space"
value: "2"
# Core dump 限制
- name: "fs.suid_dumpable"
value: "0"
# IPv6 停用 (如果不使用的話)
- name: "net.ipv6.conf.all.disable_ipv6"
value: "{{ '1' if disable_ipv6 | default(false) else '0' }}"
# TCP SYN cookies 防護
- name: "net.ipv4.tcp_syncookies"
value: "1"
# ICMP 重導向防護
- name: "net.ipv4.conf.all.accept_source_route"
value: "0"
- name: "net.ipv4.conf.default.accept_source_route"
value: "0"
# TCP 時間戳
- name: "net.ipv4.tcp_timestamps"
value: "0"
when: cis_level | string == "2"
tags: ['sysctl', 'cis_level2']
- name: 設定 limits.conf 安全限制
template:
src: security_limits.conf.j2
dest: /etc/security/limits.d/99-security-hardening.conf
owner: root
group: root
mode: '0644'
tags: ['limits']
- name: 停用不必要的網路協定
template:
src: blacklist_modules.conf.j2
dest: /etc/modprobe.d/blacklist-security.conf
owner: root
group: root
mode: '0644'
notify: update initramfs
tags: ['modules']
---
# roles/security_hardening/tasks/logging_monitoring.yml
- name: 配置 rsyslog 安全日誌
template:
src: rsyslog_security.conf.j2
dest: /etc/rsyslog.d/99-security-hardening.conf
owner: root
group: root
mode: '0644'
notify: restart rsyslog
tags: ['rsyslog']
- name: 設定日誌檔案權限
file:
path: "{{ item.path }}"
owner: "{{ item.owner | default('root') }}"
group: "{{ item.group | default('adm') }}"
mode: "{{ item.mode }}"
loop:
- { path: '/var/log/auth.log', mode: '0640' }
- { path: '/var/log/syslog', mode: '0640' }
- { path: '/var/log/kern.log', mode: '0640' }
- { path: '/var/log/messages', mode: '0640' }
ignore_errors: yes # 某些檔案可能不存在
tags: ['log_permissions']
- name: 安裝和配置 Fail2Ban
block:
- name: 安裝 Fail2Ban
package:
name: fail2ban
state: present
- name: 配置 Fail2Ban jail
template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
notify: restart fail2ban
- name: 建立自定義 Fail2Ban filter
template:
src: "{{ item }}.conf.j2"
dest: "/etc/fail2ban/filter.d/{{ item }}.conf"
owner: root
group: root
mode: '0644'
loop:
- nginx-limit-req
- apache-auth
- custom-ssh
notify: restart fail2ban
- name: 啟用 Fail2Ban 服務
systemd:
name: fail2ban
state: started
enabled: yes
when: fail2ban_enabled | bool
tags: ['fail2ban']
- name: 配置 auditd 系統審計
block:
- name: 安裝 auditd
package:
name: "{{ audit_packages[ansible_os_family] | default(['auditd']) }}"
state: present
- name: 配置 audit 規則
template:
src: audit.rules.j2
dest: /etc/audit/rules.d/99-security-hardening.rules
owner: root
group: root
mode: '0640'
notify: restart auditd
- name: 配置 auditd.conf
template:
src: auditd.conf.j2
dest: /etc/audit/auditd.conf
owner: root
group: root
mode: '0640'
notify: restart auditd
- name: 啟用 auditd 服務
systemd:
name: auditd
state: started
enabled: yes
when: audit_enabled | bool
tags: ['audit']
# roles/security_hardening/templates/jail.local.j2
# Managed by Ansible – DO NOT EDIT
[DEFAULT]
# 預設設定
bantime = {{ fail2ban_bantime | default('3600') }}
findtime = {{ fail2ban_findtime | default('600') }}
maxretry = {{ fail2ban_maxretry | default('5') }}
backend = auto
usedns = warn
# 通知設定 (可選)
{% if fail2ban_email is defined %}
destemail = {{ fail2ban_email }}
sendername = Fail2Ban-{{ inventory_hostname }}
mta = sendmail
action = %(action_mwl)s
{% else %}
action = %(action_)s
{% endif %}
[sshd]
enabled = true
port = {{ ssh_port }}
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
findtime = 300
[nginx-http-auth]
enabled = {{ 'true' if 'nginx' in group_names else 'false' }}
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
[nginx-noscript]
enabled = {{ 'true' if 'nginx' in group_names else 'false' }}
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 6
[nginx-badbots]
enabled = {{ 'true' if 'nginx' in group_names else 'false' }}
filter = apache-badbots
logpath = /var/log/nginx/access.log
maxretry = 2
[nginx-noproxy]
enabled = {{ 'true' if 'nginx' in group_names else 'false' }}
filter = nginx-noproxy
logpath = /var/log/nginx/access.log
maxretry = 2
# 自定義過濾器
[custom-ssh]
enabled = true
filter = custom-ssh
logpath = /var/log/auth.log
maxretry = 2
bantime = 86400 # 24 小時
---
# roles/security_hardening/tasks/compliance_check.yml
- name: 建立合規檢查目錄
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: '0750'
loop:
- /var/log/compliance
- /opt/security-scripts
tags: ['setup']
- name: 部署 CIS Benchmark 檢查腳本
template:
src: cis_check.sh.j2
dest: /opt/security-scripts/cis_check.sh
owner: root
group: root
mode: '0750'
tags: ['scripts']
- name: 執行 CIS Benchmark 檢查
command: /opt/security-scripts/cis_check.sh
register: cis_check_result
changed_when: false
failed_when: false
tags: ['cis_check']
- name: 生成合規報告
template:
src: compliance_report.html.j2
dest: "/var/log/compliance/compliance_report_{{ ansible_date_time.date }}.html"
owner: root
group: root
mode: '0644'
vars:
check_timestamp: "{{ ansible_date_time.iso8601 }}"
cis_results: "{{ cis_check_result.stdout_lines | default([]) }}"
tags: ['report']
- name: 檢查關鍵安全項目
block:
# 檢查是否有弱密碼用戶
- name: 檢查系統用戶密碼政策
shell: |
awk -F: '($2 != "x" && $2 != "*" && $2 != "!") { print $1 }' /etc/passwd
register: weak_password_users
changed_when: false
# 檢查 SUID/SGID 檔案
- name: 查找系統中的 SUID/SGID 檔案
shell: |
find /usr -xdev -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null
register: suid_files
changed_when: false
# 檢查監聽端口
- name: 檢查系統監聽端口
shell: |
ss -tulpn | grep LISTEN
register: listening_ports
changed_when: false
# 檢查失敗的登入嘗試
- name: 檢查最近的失敗登入
shell: |
lastb -n 20 | head -n -2 || echo "No failed logins"
register: failed_logins
changed_when: false
tags: ['security_audit']
- name: 記錄安全檢查結果
lineinfile:
path: /var/log/compliance/security_audit.log
line: |
{{ ansible_date_time.iso8601 }} - {{ inventory_hostname }}
弱密碼用戶: {{ weak_password_users.stdout_lines | length }}
SUID 檔案數量: {{ suid_files.stdout_lines | length }}
監聽端口數量: {{ listening_ports.stdout_lines | length }}
最近失敗登入: {{ failed_logins.stdout_lines | length }}
create: yes
tags: ['logging']
- name: 發送安全報告 (可選)
mail:
to: "{{ security_report_email | default('admin@company.com') }}"
subject: "安全合規檢查報告 - {{ inventory_hostname }}"
body: |
主機: {{ inventory_hostname }}
檢查時間: {{ ansible_date_time.iso8601 }}
檢查結果摘要:
- 弱密碼用戶: {{ weak_password_users.stdout_lines | length }} 個
- SUID 檔案: {{ suid_files.stdout_lines | length }} 個
- 監聽端口: {{ listening_ports.stdout_lines | length }} 個
- 失敗登入嘗試: {{ failed_logins.stdout_lines | length }} 次
詳細報告請查看附件。
attach: "/var/log/compliance/compliance_report_{{ ansible_date_time.date }}.html"
delegate_to: localhost
when: security_report_email is defined and send_email_reports | default(false)
tags: ['notification']
---
# roles/security_hardening/handlers/main.yml
- name: restart sshd
systemd:
name: sshd
state: restarted
listen: restart sshd
- name: restart rsyslog
systemd:
name: rsyslog
state: restarted
listen: restart rsyslog
- name: restart fail2ban
systemd:
name: fail2ban
state: restarted
listen: restart fail2ban
- name: restart auditd
systemd:
name: auditd
state: restarted
listen: restart auditd
- name: update initramfs
command: update-initramfs -u
listen: update initramfs
---
# security-hardening.yml
- name: 系統安全強化部署
hosts: all
become: yes
vars:
# 可以在這裡覆寫 role 的預設值
ssh_port: 2222
firewall_allowed_ports:
- "2222/tcp" # 自定義 SSH 端口
- "80/tcp"
- "443/tcp"
- "8080/tcp" # 應用程式端口
pre_tasks:
- name: 檢查執行權限
assert:
that:
- ansible_user_id != "root" or allow_root_execution | default(false)
fail_msg: "不建議用 root 執行此 Playbook,請使用有 sudo 權限的一般用戶"
- name: 確認系統支援性
assert:
that:
- ansible_distribution in ['Ubuntu', 'Debian', 'CentOS', 'RedHat']
- ansible_distribution_major_version | int >= 18
fail_msg: "不支援的作業系統或版本太舊"
- name: 建立執行日誌
lineinfile:
path: /var/log/security-hardening.log
line: "{{ ansible_date_time.iso8601 }} - 開始安全強化 by {{ ansible_user }}"
create: yes
delegate_to: localhost
roles:
- security_hardening
post_tasks:
- name: 執行最終安全檢查
include_role:
name: security_hardening
tasks_from: compliance_check.yml
- name: 顯示重要安全資訊
debug:
msg:
- "=== 安全強化完成 ==="
- "SSH 端口已變更為: {{ ssh_port }}"
- "請確認能夠透過新端口連線後再中斷目前連線"
- "防火牆已啟用,允許端口: {{ firewall_allowed_ports | join(', ') }}"
- "Fail2Ban 已啟用,保護關鍵服務"
- "系統審計已啟用,請定期檢查 /var/log/audit/"
- "合規報告位於: /var/log/compliance/"
- name: 最終提醒
pause:
prompt: |
安全強化已完成!請注意:
1. SSH 端口已改為 {{ ssh_port }},請確認防火牆允許此端口
2. 強烈建議重新啟動系統以確保所有設定生效
3. 請保留備份的配置檔案以備不時之需
是否要立即重新啟動系統?(yes/no)
register: reboot_confirmation
when: prompt_for_reboot | default(true)
- name: 重新啟動系統
reboot:
msg: "系統安全強化後重新啟動"
reboot_timeout: 300
when:
- reboot_confirmation is defined
- reboot_confirmation.user_input | lower in ['yes', 'y']
# group_vars/production.yml
---
cis_level: "2" # 生產環境使用更嚴格的安全標準
ssh_port: 2222 # 非標準端口
ssh_permit_root_login: false # 禁止 root 登入
firewall_logging: "full" # 完整防火牆日誌
audit_enabled: true # 啟用系統審計
fail2ban_enabled: true # 啟用入侵防護
send_email_reports: true # 發送安全報告
security_report_email: "security@company.com"
# 生產環境額外的監控
additional_monitoring:
- ossec
- tripwire
- rkhunter
# group_vars/development.yml
---
cis_level: "1" # 開發環境使用基本安全標準
ssh_permit_root_login: true # 允許 root (僅開發環境)
firewall_logging: "low" # 減少日誌量
audit_enabled: false # 開發環境不啟用審計
send_email_reports: false # 不發送報告
# 開發環境可能需要的額外端口
firewall_allowed_ports:
- "22/tcp"
- "80/tcp"
- "443/tcp"
- "3000/tcp" # React dev server
- "8000/tcp" # Django dev server
- "5432/tcp" # PostgreSQL
---
# security-monitoring.yml
- name: 部署安全監控
hosts: monitoring
become: yes
tasks:
- name: 建立安全指標收集腳本
template:
src: security_metrics.sh.j2
dest: /opt/monitoring/security_metrics.sh
owner: root
group: root
mode: '0755'
- name: 設定定時執行安全檢查
cron:
name: "Security metrics collection"
minute: "*/15"
job: "/opt/monitoring/security_metrics.sh"
user: root
- name: 設定每日安全報告
cron:
name: "Daily security report"
minute: "0"
hour: "8"
job: "/opt/security-scripts/daily_security_report.sh"
user: root
- name: 建立安全事件告警規則
template:
src: security_alerts.yml.j2
dest: /etc/prometheus/rules/security_alerts.yml
owner: prometheus
group: prometheus
mode: '0644'
notify: reload prometheus
when: prometheus_enabled | default(false)
經過今天的學習,相信大家對自動化安全管理有了全面的認識,筆者想分享幾個重要心得:
記住,安全不是目的,而是手段。我們的目標是在保障安全的前提下,讓系統穩定高效地運行。過度的安全限制可能會影響業務,適度的安全配置才是最好的選擇。
明天我們將學習效能監控與擴展,了解如何監控系統效能並實作自動擴展機制!