Phase 5: backup/DR automation (backup Ansible role)
Implements the two decoupled backup streams from docs/06 as an idempotent
Ansible role wired into the host playbook:
- Files: sanoid takes/prunes ZFS snapshots per policy (sanoid_datasets) on
its packaged timer; syncoid replicates offsite (heleos-zfs-offsite),
enabled only when zfs_offsite_target is set.
- DB: heleos-db-backup (nightly systemd timer) walks the deployments dir and
dumps each DB-backed site via `docker exec mariadb-dump` into
db-backups/<customer>/<site>/{daily,weekly,monthly} with rotation
(automysqlbackup-style, adapted for the containerized DB; MYSQL_PWD keeps
the password out of the process list). heleos-db-offsite rsyncs offsite
when db_offsite_target is set.
Streams and schedules are configured in group_vars/all.yml; offsite is
opt-in via the two target vars. Updates doc 06 (implementation note), the
ansible README, and CLAUDE.md status. YAML + templates validated by render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fe4cbbe36a
commit
0ade1c740f
11 changed files with 318 additions and 3 deletions
|
|
@ -26,8 +26,9 @@ host, Docker Compose per site, no orchestrator.
|
||||||
- ✅ Phase 2 — platform services (Traefik, MariaDB, Forgejo) in [platform-infra/stacks/](platform-infra/stacks/README.md)
|
- ✅ Phase 2 — platform services (Traefik, MariaDB, Forgejo) in [platform-infra/stacks/](platform-infra/stacks/README.md)
|
||||||
- ✅ Phase 3 — site templates & base images in [site-templates/](site-templates/README.md)
|
- ✅ Phase 3 — site templates & base images in [site-templates/](site-templates/README.md)
|
||||||
- ✅ Phase 4 — provisioning CLI `heleosctl` in [control-panel/](control-panel/README.md) (provision/deprovision/list/render/backup/restore; `--dry-run`; 22 tests)
|
- ✅ Phase 4 — provisioning CLI `heleosctl` in [control-panel/](control-panel/README.md) (provision/deprovision/list/render/backup/restore; `--dry-run`; 22 tests)
|
||||||
- 🚧 Phase 5 — backup/DR automation (ZFS snapshot/`send` + automysqlbackup jobs, offsite, restore drill)
|
- ✅ Phase 5 — backup/DR automation in the `backup` Ansible role (sanoid snapshots, per-DB dump timer, optional syncoid/rsync offsite)
|
||||||
- ⬜ Phases 6–8 — observability, migration, panel
|
- 🚧 Phase 6 — observability (Prometheus/Grafana + cAdvisor/node_exporter/Traefik metrics, Loki/Promtail, Uptime-Kuma)
|
||||||
|
- ⬜ Phases 7–8 — migration, customer panel
|
||||||
|
|
||||||
### Phase 4 follow-ups (not yet done)
|
### Phase 4 follow-ups (not yet done)
|
||||||
- `reconfigure` + `rotate-secret` commands; git-commit of `deployments/` on provision; php-fpm umask for two-way SFTP editing (see control-panel/README.md).
|
- `reconfigure` + `rotate-secret` commands; git-commit of `deployments/` on provision; php-fpm umask for two-way SFTP editing (see control-panel/README.md).
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,21 @@ config) is backed up with the same ZFS mechanism from `tank/platform/*`.
|
||||||
- **DB dumps:** rsync to the same or another offsite location (delta transfer).
|
- **DB dumps:** rsync to the same or another offsite location (delta transfer).
|
||||||
- Both offsite copies are the recovery source if the primary host is lost.
|
- Both offsite copies are the recovery source if the primary host is lost.
|
||||||
|
|
||||||
|
## 2b. Implementation (Phase 5)
|
||||||
|
|
||||||
|
Automated by the `backup` Ansible role (`platform-infra/ansible/roles/backup`):
|
||||||
|
|
||||||
|
- **Files:** [sanoid](https://github.com/jimsalterjrs/sanoid) takes and prunes
|
||||||
|
snapshots per policy (`sanoid.conf`, driven by `sanoid_datasets`), on the
|
||||||
|
packaged `sanoid.timer`. **syncoid** replicates offsite
|
||||||
|
(`heleos-zfs-offsite`), enabled only when `zfs_offsite_target` is set.
|
||||||
|
- **DB:** `heleos-db-backup` (systemd timer, nightly) walks the deployments dir,
|
||||||
|
dumps each DB-backed site via `docker exec … mariadb-dump` into
|
||||||
|
`db-backups/<customer>/<site>/{daily,weekly,monthly}` with rotation, then
|
||||||
|
`heleos-db-offsite` rsyncs offsite (enabled only when `db_offsite_target` set).
|
||||||
|
- Toggle streams with `backup_snapshots_enabled` / `db_backup_enabled` and the
|
||||||
|
offsite target vars in `group_vars/all.yml`.
|
||||||
|
|
||||||
## 3. Restore — single site (the critical drill)
|
## 3. Restore — single site (the critical drill)
|
||||||
|
|
||||||
Two coordinated steps:
|
Two coordinated steps:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
Idempotent host configuration for the heleos platform, targeting **Ubuntu 24.04
|
Idempotent host configuration for the heleos platform, targeting **Ubuntu 24.04
|
||||||
LTS**. Roles: base packages → ZFS pool/datasets → Docker (data-root on ZFS) →
|
LTS**. Roles: base packages → ZFS pool/datasets → Docker (data-root on ZFS) →
|
||||||
nftables firewall + container egress filter → SSH hardening.
|
nftables firewall + container egress filter → SSH hardening → backup/DR
|
||||||
|
(sanoid snapshots, per-DB dumps, offsite).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|
@ -67,6 +68,9 @@ Run a single layer with tags: `--tags zfs`, `--tags docker`, `--tags firewall`,
|
||||||
ICMP); outbound SMTP blocked from containers.
|
ICMP); outbound SMTP blocked from containers.
|
||||||
- Hardened SSH (key-first, root prohibit-password) and the `sftponly` group that
|
- Hardened SSH (key-first, root prohibit-password) and the `sftponly` group that
|
||||||
per-customer SFTP accounts will join in Phase 4.
|
per-customer SFTP accounts will join in Phase 4.
|
||||||
|
- Backup/DR (Phase 5): sanoid snapshot policy + timer, nightly per-database dumps
|
||||||
|
with rotation, and optional offsite `zfs send` (syncoid) / rsync — enable
|
||||||
|
offsite by setting `zfs_offsite_target` / `db_offsite_target`.
|
||||||
|
|
||||||
## Verify after running
|
## Verify after running
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,29 @@ smtp_relay_host: "" # optional: allow SMTP only to this hos
|
||||||
admin_user: "{{ ansible_user }}"
|
admin_user: "{{ ansible_user }}"
|
||||||
admin_authorized_keys: [] # list of public key strings
|
admin_authorized_keys: [] # list of public key strings
|
||||||
ssh_disable_password_auth: false # flip to true once key login is verified
|
ssh_disable_password_auth: false # flip to true once key login is verified
|
||||||
|
|
||||||
|
# --- Backups / DR (Phase 5) -------------------------------------------------
|
||||||
|
mariadb_container: mariadb
|
||||||
|
mariadb_env_file: /opt/heleos/platform-infra/stacks/mariadb/.env
|
||||||
|
deployments_dir: /opt/heleos/deployments
|
||||||
|
|
||||||
|
# ZFS snapshots via sanoid (files stream). Retention is per policy below.
|
||||||
|
backup_snapshots_enabled: true
|
||||||
|
sanoid_datasets:
|
||||||
|
- { name: "{{ zfs_pool_name }}/customers", recursive: true, hourly: 36, daily: 30, weekly: 8, monthly: 6 }
|
||||||
|
- { name: "{{ zfs_pool_name }}/platform", recursive: true, hourly: 0, daily: 14, weekly: 4, monthly: 3 }
|
||||||
|
|
||||||
|
# Per-database dumps (DB stream), automysqlbackup-style rotation via docker exec.
|
||||||
|
db_backup_enabled: true
|
||||||
|
db_backup_dir: "/{{ zfs_pool_name }}/platform/db-backups"
|
||||||
|
db_backup_oncalendar: "*-*-* 01:30:00"
|
||||||
|
db_backup_keep_daily: 14
|
||||||
|
db_backup_keep_weekly: 8
|
||||||
|
db_backup_keep_monthly: 6
|
||||||
|
|
||||||
|
# Offsite. Leave the targets empty to disable that stream.
|
||||||
|
zfs_offsite_target: "" # e.g. "user@backup-host:backup/heleos"
|
||||||
|
zfs_offsite_datasets: ["{{ zfs_pool_name }}/customers", "{{ zfs_pool_name }}/platform"]
|
||||||
|
zfs_offsite_oncalendar: "*-*-* 03:00:00"
|
||||||
|
db_offsite_target: "" # e.g. "user@backup-host:/srv/heleos/db-backups"
|
||||||
|
db_offsite_oncalendar: "*-*-* 03:30:00"
|
||||||
|
|
|
||||||
4
platform-infra/ansible/roles/backup/handlers/main.yml
Normal file
4
platform-infra/ansible/roles/backup/handlers/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Reload systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
175
platform-infra/ansible/roles/backup/tasks/main.yml
Normal file
175
platform-infra/ansible/roles/backup/tasks/main.yml
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
---
|
||||||
|
# ── ZFS snapshots (files stream) via sanoid ─────────────────────────────────
|
||||||
|
- name: Install sanoid (snapshots + syncoid replication)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: sanoid
|
||||||
|
state: present
|
||||||
|
when: backup_snapshots_enabled
|
||||||
|
|
||||||
|
- name: Configure sanoid snapshot policy
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sanoid.conf.j2
|
||||||
|
dest: /etc/sanoid/sanoid.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
when: backup_snapshots_enabled
|
||||||
|
|
||||||
|
- name: Enable the sanoid timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sanoid.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
when: backup_snapshots_enabled
|
||||||
|
|
||||||
|
# ── Per-database dumps (DB stream) ──────────────────────────────────────────
|
||||||
|
- name: Install the DB backup script
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: heleos-db-backup.sh.j2
|
||||||
|
dest: /usr/local/sbin/heleos-db-backup.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
when: db_backup_enabled
|
||||||
|
|
||||||
|
- name: Install the DB backup service + timer
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/systemd/system/{{ item.name }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: "{{ item.content }}"
|
||||||
|
loop:
|
||||||
|
- name: heleos-db-backup.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=heleos per-database dumps (rotated)
|
||||||
|
After=docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/heleos-db-backup.sh
|
||||||
|
- name: heleos-db-backup.timer
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Run heleos DB backups on a schedule
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ db_backup_oncalendar }}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
when: db_backup_enabled
|
||||||
|
notify: Reload systemd
|
||||||
|
|
||||||
|
- name: Enable the DB backup timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: heleos-db-backup.timer
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
when: db_backup_enabled
|
||||||
|
|
||||||
|
# ── Offsite: files (syncoid / zfs send) ─────────────────────────────────────
|
||||||
|
- name: Install the ZFS offsite script
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: heleos-zfs-offsite.sh.j2
|
||||||
|
dest: /usr/local/sbin/heleos-zfs-offsite.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
when: zfs_offsite_target | length > 0
|
||||||
|
|
||||||
|
- name: Install the ZFS offsite service + timer
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/systemd/system/{{ item.name }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: "{{ item.content }}"
|
||||||
|
loop:
|
||||||
|
- name: heleos-zfs-offsite.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=heleos offsite ZFS replication (syncoid)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/heleos-zfs-offsite.sh
|
||||||
|
- name: heleos-zfs-offsite.timer
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Run heleos offsite ZFS replication on a schedule
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ zfs_offsite_oncalendar }}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
when: zfs_offsite_target | length > 0
|
||||||
|
notify: Reload systemd
|
||||||
|
|
||||||
|
- name: Enable the ZFS offsite timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: heleos-zfs-offsite.timer
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
when: zfs_offsite_target | length > 0
|
||||||
|
|
||||||
|
# ── Offsite: DB dumps (rsync) ───────────────────────────────────────────────
|
||||||
|
- name: Install the DB offsite script
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: heleos-db-offsite.sh.j2
|
||||||
|
dest: /usr/local/sbin/heleos-db-offsite.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
when: db_offsite_target | length > 0
|
||||||
|
|
||||||
|
- name: Install the DB offsite service + timer
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "/etc/systemd/system/{{ item.name }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: "{{ item.content }}"
|
||||||
|
loop:
|
||||||
|
- name: heleos-db-offsite.service
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=heleos offsite DB dump sync (rsync)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/heleos-db-offsite.sh
|
||||||
|
- name: heleos-db-offsite.timer
|
||||||
|
content: |
|
||||||
|
[Unit]
|
||||||
|
Description=Run heleos offsite DB sync on a schedule
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar={{ db_offsite_oncalendar }}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
||||||
|
when: db_offsite_target | length > 0
|
||||||
|
notify: Reload systemd
|
||||||
|
|
||||||
|
- name: Enable the DB offsite timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: heleos-db-offsite.timer
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
state: started
|
||||||
|
when: db_offsite_target | length > 0
|
||||||
51
platform-infra/ansible/roles/backup/templates/heleos-db-backup.sh.j2
Executable file
51
platform-infra/ansible/roles/backup/templates/heleos-db-backup.sh.j2
Executable file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# heleos per-database dumps — automysqlbackup-style rotation, adapted for the
|
||||||
|
# containerized shared MariaDB. Managed by Ansible. Iterates the deployments dir
|
||||||
|
# (source of truth) and dumps each DB-backed site into
|
||||||
|
# db-backups/<customer>/<site>/{daily,weekly,monthly}.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER="{{ mariadb_container }}"
|
||||||
|
BASE="{{ db_backup_dir }}"
|
||||||
|
DEPLOYMENTS="{{ deployments_dir }}"
|
||||||
|
KEEP_DAILY={{ db_backup_keep_daily }}
|
||||||
|
KEEP_WEEKLY={{ db_backup_keep_weekly }}
|
||||||
|
KEEP_MONTHLY={{ db_backup_keep_monthly }}
|
||||||
|
|
||||||
|
# Root password: env var wins, else read from the mariadb stack .env.
|
||||||
|
ROOTPW="${MARIADB_ROOT_PASSWORD:-}"
|
||||||
|
if [ -z "$ROOTPW" ] && [ -f "{{ mariadb_env_file }}" ]; then
|
||||||
|
ROOTPW="$(grep -E '^MARIADB_ROOT_PASSWORD=' "{{ mariadb_env_file }}" | cut -d= -f2-)"
|
||||||
|
fi
|
||||||
|
if [ -z "$ROOTPW" ]; then
|
||||||
|
echo "no MariaDB root password (env or {{ mariadb_env_file }})" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dow="$(date +%u)" # 7 = Sunday
|
||||||
|
dom="$(date +%d)" # 01 = first of month
|
||||||
|
ts="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for meta in "$DEPLOYMENTS"/*/*/site.yaml; do
|
||||||
|
d="$(dirname "$meta")"
|
||||||
|
site="$(basename "$d")"
|
||||||
|
customer="$(basename "$(dirname "$d")")"
|
||||||
|
grep -qiE '^database:[[:space:]]*true' "$meta" || continue
|
||||||
|
|
||||||
|
db="db_${customer//-/_}_${site//-/_}"
|
||||||
|
dest="$BASE/$customer/$site"
|
||||||
|
mkdir -p "$dest/daily" "$dest/weekly" "$dest/monthly"
|
||||||
|
|
||||||
|
out="$dest/daily/${db}-${ts}.sql.gz"
|
||||||
|
# MYSQL_PWD keeps the password out of the container's process list.
|
||||||
|
docker exec -e MYSQL_PWD="$ROOTPW" "$CONTAINER" \
|
||||||
|
mariadb-dump --single-transaction --databases "$db" -uroot | gzip > "$out"
|
||||||
|
|
||||||
|
[ "$dow" = "7" ] && cp -f "$out" "$dest/weekly/"
|
||||||
|
[ "$dom" = "01" ] && cp -f "$out" "$dest/monthly/"
|
||||||
|
|
||||||
|
find "$dest/daily" -name '*.sql.gz' -type f -mtime +"$KEEP_DAILY" -delete
|
||||||
|
find "$dest/weekly" -name '*.sql.gz' -type f -mtime +"$(( KEEP_WEEKLY * 7 ))" -delete
|
||||||
|
find "$dest/monthly" -name '*.sql.gz' -type f -mtime +"$(( KEEP_MONTHLY * 31 ))" -delete
|
||||||
|
done
|
||||||
6
platform-infra/ansible/roles/backup/templates/heleos-db-offsite.sh.j2
Executable file
6
platform-infra/ansible/roles/backup/templates/heleos-db-offsite.sh.j2
Executable file
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# heleos offsite DB dump sync via rsync. Managed by Ansible. Only installed/
|
||||||
|
# enabled when db_offsite_target is set. Dumps are already rotated locally.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
rsync -a --delete "{{ db_backup_dir }}/" "{{ db_offsite_target }}/"
|
||||||
14
platform-infra/ansible/roles/backup/templates/heleos-zfs-offsite.sh.j2
Executable file
14
platform-infra/ansible/roles/backup/templates/heleos-zfs-offsite.sh.j2
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# heleos offsite ZFS replication via syncoid (incremental zfs send). Managed by
|
||||||
|
# Ansible. Only installed/enabled when zfs_offsite_target is set.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET="{{ zfs_offsite_target }}"
|
||||||
|
|
||||||
|
for ds in {{ zfs_offsite_datasets | join(' ') }}; do
|
||||||
|
# DEST mirrors the last path component under the offsite base, e.g.
|
||||||
|
# tank/customers -> <target>/customers
|
||||||
|
dest="${TARGET}/${ds##*/}"
|
||||||
|
echo "==> syncoid ${ds} -> ${dest}"
|
||||||
|
syncoid --recursive --no-sync-snap "${ds}" "${dest}"
|
||||||
|
done
|
||||||
17
platform-infra/ansible/roles/backup/templates/sanoid.conf.j2
Normal file
17
platform-infra/ansible/roles/backup/templates/sanoid.conf.j2
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# heleos sanoid snapshot policy — managed by Ansible.
|
||||||
|
# Files stream only; DB dumps are handled separately (heleos-db-backup).
|
||||||
|
{% for d in sanoid_datasets %}
|
||||||
|
[{{ d.name }}]
|
||||||
|
use_template = {{ d.name | replace('/', '_') }}
|
||||||
|
recursive = {{ 'yes' if d.recursive else 'no' }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for d in sanoid_datasets %}
|
||||||
|
[template_{{ d.name | replace('/', '_') }}]
|
||||||
|
hourly = {{ d.hourly }}
|
||||||
|
daily = {{ d.daily }}
|
||||||
|
weekly = {{ d.weekly }}
|
||||||
|
monthly = {{ d.monthly }}
|
||||||
|
autosnap = yes
|
||||||
|
autoprune = yes
|
||||||
|
{% endfor %}
|
||||||
|
|
@ -20,3 +20,5 @@
|
||||||
tags: [firewall]
|
tags: [firewall]
|
||||||
- role: ssh_hardening
|
- role: ssh_hardening
|
||||||
tags: [ssh]
|
tags: [ssh]
|
||||||
|
- role: backup
|
||||||
|
tags: [backup]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue