我想使用 Ansible 管理多台远程 Ubuntu 18.04 机器上的 UFW 防火墙规则。如果防火墙规则的更改阻止我通过 SSH 重新连接到机器,这将很难修复(匆忙前往数据中心,逐个输入复杂的 root 密码,手动编辑防火墙配置)。有没有办法在应用更改之前测试防火墙规则更改不会阻止我重新连接?
或者,如果应用了防火墙规则并且我被锁定,有没有办法自动恢复它们?(我可以自己备份并设置一个 cron 作业来恢复它,然后再次连接并删除该 cron 作业,但也许这样的事情已经存在了?)
答案1
未内置于 ufw 模块中。并且您所做的更改将在下次重新启动或防火墙重新加载时生效。
您可以做的是重新加载防火墙,然后测试与 SSH 端口的新连接。如果失败,请通过仍然打开的持久连接重置 ufw。
我有一个实现这个无聊的叫做ansible-角色-ufw。请特别注意 的使用wait_for
,因为wait_for_connection
将使用持久连接并且不会检测到故障。
请注意,此方法只有一次成功的机会。当 SSH 损坏时,您仍然需要远程控制台访问。
答案2
手动应用规则,不保存,但在执行之前,安排几分钟后重新启动或重置当前规则。因此,如果新规则会造成任何损害,也只会持续几分钟。
答案3
这就是我最终的结果,延伸约翰·马霍瓦尔德的代码:
角色/set_firewall_rules/任务/main.yml
# Apply all the requested firewall rules, then try to establish a new SSH connection to the host.
# If that SSH connection fails then reset the firewall, so the user is not locked out of the machine!
# Make sure the SSH connection details figured out by target_ssh_info can actually be used to connect before the change.
# If they're not we'd end up resetting the firewall after ANY change.
- name: Try to SSH before updating firewall
become: no
wait_for:
host: "{{ target_ssh_host }}"
port: "{{ target_ssh_port }}"
search_regex: SSH
timeout: 5
msg: "Failed to connect to {{ target_ssh_host }}:{{ target_ssh_port }} before firewall rule change"
connection: local
- name: Set firewall rules
ufw:
src: "{{ item.src }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
rule: "{{ item.rule }}"
comment: "{{ item.comment }}"
register: firewall_rules
loop: "{{ rules }}"
# Enable/reload the firewall as a separate task, after all rules have been added, so that the order of rules doesn't matter, i.e. we're not locked out
# if a deny rule comes before an allow rule (as it should).
- name: Enable and reload firewall
ufw:
state: enabled
register: firewall_enabled
- name: Try to SSH after updating firewall
become: no
# wait_for is key here: it establishes a new connection, while wait_for_connection would re-use the existing one
wait_for:
host: "{{ target_ssh_host }}"
port: "{{ target_ssh_port }}"
search_regex: SSH
timeout: 5
msg: "Failed to connect to {{ target_ssh_host }}:{{ target_ssh_port }} after firewall rule change, trying to reset ufw"
when: firewall_rules.changed or firewall_enabled.changed
connection: local
ignore_errors: yes
register: ssh_after_ufw_change
# Reset the firewall if the new connection failed above. This works (mostly!), because it uses the existing connection
- name: Reset firewall if unable to SSH
ufw:
state: reset
when:
- firewall_rules.changed or firewall_enabled.changed
- ssh_after_ufw_change.failed
# Stop the playbook - the host is now open to the world (firewall is off), which the user really needs to fix ASAP.
# It's probably better than being locked out of it, though!
- name: Fail if unable to SSH after firewall change
fail:
msg: "Locked out of SSH after firewall rule changes - firewall was reset"
when:
- firewall_rules.changed or firewall_enabled.changed
- ssh_after_ufw_change.failed
角色/set_firewall_rules/meta/main.yml
---
dependencies:
- { role: target_ssh_info }
角色/target_ssh_info/任务/main.yml
# Set target_ssh_host and target_ssh_port facts to the real hostname and port SSH uses to connect.
# ansible_host and ansible_port can be set at the host level to define what Ansible passes to ssh, but ssh then looks up ansible_host in ~/.ssh/config.
# This role figure out the real hostname it then connects to - useful for establishing a non-SSH connection to the same host.
# ansible_port is similar, but a little different: if set it overrides the value in ~/.ssh/config.
- name: Get hostname from local SSH config
shell: "ssh -G '{{ ansible_host | default(inventory_hostname) }}' | awk '/^hostname / { print $2 }'"
connection: local
become: no
register: ssh_host
changed_when: false
- name: Get port from local SSH config
shell: "ssh -G '{{ ansible_host | default(inventory_hostname) }}' | awk '/^port / { print $2 }'"
connection: local
become: no
register: ssh_port
changed_when: false
when: ansible_port is not defined
# ansible_port overrides whatever is set in .ssh/config
- name: Set target SSH host and port
set_fact:
target_ssh_host: "{{ ssh_host.stdout }}"
target_ssh_port: "{{ ansible_port | default (ssh_port.stdout) }}"
请注意,ssh -G
即使主机名和端口没有在 .ssh/config 中被覆盖,也会返回它们,即ssh -G arbitrarystring
只返回“arbitrarystring”作为主机名,返回 22 作为端口。