Я хочу управлять правилами брандмауэра UFW на нескольких удаленных машинах Ubuntu 18.04 с помощью Ansible. Если изменение правил брандмауэра не позволяет мне повторно подключиться к машинам через SSH, это будет очень сложно исправить (спешите в центр обработки данных, введите сложные пароли root один за другим, отредактируйте конфигурацию брандмауэра вручную). Есть ли способ проверить, что изменение правила брандмауэра не помешает мне повторно подключиться до того, как изменение вступит в силу?
В качестве альтернативы, есть ли способ автоматически восстановить правила брандмауэра, если они применяются, а я заблокирован? (Я мог бы сделать свою резервную копию и настроить задание cron для его восстановления, затем снова подключиться и удалить задание cron, но, может быть, что-то подобное уже существует?)
Не встроен в модуль ufw. И внесенные вами изменения будут применены при следующей перезагрузке или перезагрузке брандмауэра.
Что вы можете сделать, так это перезагрузить брандмауэр, а затем протестировать новое соединение с портом SSH. Если это не удается, сбросьте ufw через постоянное соединение, которое все еще открыто.
У меня есть реализация этого скучно названного анзибль-роль-ufw. Обратите внимание, в частности, на использование wait_for
, так как wait_for_connection
будет использовать постоянное соединение и не обнаружит сбой.
Помните, что у этого есть только один шанс. Вам по-прежнему нужен доступ к удаленной консоли, когда SSH не работает.
Примените правила вручную, не сохраняя их, но перед этим запланируйте перезагрузку или сброс текущих правил через пару минут. Так что, если новое правило причинит вред, то только на эти пару минут.
Вот что у меня получилось, расширив Джон Маховальдкод:
# 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
---
dependencies:
- { role: target_ssh_info }
# 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
просто возвращает «произвольную строку» в качестве имени хоста и 22 в качестве порта.