Back to Learn

FortiGate firewall backup automation with Ansible

A modular playbook that backs up multiple FortiGate devices dynamically, organizes backups, and applies automation best practices.

Fortinet
Ansible
Network Automation
FortiGate Backup Workflow
Dynamic, multi-device inventory
Automatic retention management
Env-based credentials & audit log

Modular playbook steps

  1. 1

    Ensure the backup directory exists

    Create the local backups folder once on the control node before fetching any configuration.

  2. 2

    Pull the configuration backup

    Use fortinet.fortios.fortios_monitor_fact with the system_config_backup selector to retrieve the full config over the HTTPS API.

  3. 3

    Save the configuration to file

    Write the raw config to a timestamped .conf file per device so every run is uniquely identifiable.

  4. 4

    Document device details

    Query system_status and store a per-device info file with site, role, version, model and serial number.

  5. 5

    Apply retention

    Find and remove .conf/.txt files older than retention_days so the backup store never grows unbounded.

  6. 6

    Log and report

    Append a SUCCESS/FAILED line to backup.log and print a clear per-device status to the console.

forti_backup.yaml
yaml
---
- name: Backup FortiGate Firewalls
  hosts: fortigates
  gather_facts: no
  connection: httpapi

  tasks:
    - name: Ensure local backup directory exists
      ansible.builtin.file:
        path: "{{ backup_dir }}"
        state: directory
        mode: '0755'
      run_once: true
      delegate_to: localhost

    - name: Backup FortiGate configuration
      fortinet.fortios.fortios_monitor_fact:
        selector: 'system_config_backup'
        params:
          scope: "{{ fortigate_backup_scope }}"
      register: backup_result
      ignore_errors: yes

    - name: Save configuration to file
      ansible.builtin.copy:
        content: "{{ backup_result.meta.raw }}"
        dest: "{{ backup_dir }}/{{ inventory_hostname }}_config_{{ backup_timestamp }}.conf"
        mode: '0640'
      delegate_to: localhost
      register: save_result
      when:
        - backup_result is succeeded
        - backup_result.meta.raw is defined

    - name: Find old backup files
      ansible.builtin.find:
        paths: "{{ backup_dir }}"
        patterns: ["*.conf", "*.txt"]
        age: "{{ retention_days }}d"
      register: old_backups
      delegate_to: localhost
      run_once: true

    - name: Remove old backups
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"
      delegate_to: localhost
      run_once: true

Device variables

Variables are layered from global defaults down to per-device overrides, so the same playbook scales from one firewall to a whole fleet.

Global (all hosts)group_vars/all.yaml
backup_dir./backups
retention_days30
backup_timestampdate +%Y-%m-%d_%H%M%S (pipe lookup)
FortiGate groupgroup_vars/fortigates/vars.yaml
ansible_network_osfortinet.fortios.fortios
ansible_connectionhttpapi
ansible_httpapi_use_sslyes
ansible_httpapi_port443
ansible_user / ansible_passwordfrom FORTIGATE_USER / FORTIGATE_PASSWORD env
fortigate_backup_scopeglobal
Per devicehost_vars/<device>.yaml
ansible_host192.168.1.10
fortigate_siteHeadquarters - Main Data Center
fortigate_locationAmsterdam, Netherlands
fortigate_rolePrimary Internet Gateway
fortigate_environmentproduction
retention_days (override)90 (optional, critical devices)

Variable schema & validation rules

These rules are enforced by a zod schema (fortigate-backup-schema.ts). Credentials must be env lookups or Ansible Vault references โ€” never literal secrets.

VariableLayerReq.Rule
backup_dirGlobalโ€”Path, no shell metacharacters; defaults to ./backups
retention_daysGlobalโ€”Integer 1โ€“3650; default 30
backup_timestampGlobalโ€”Non-empty Jinja/pipe expression for unique filenames
backup_scheduleGlobalโ€”5-field cron (m h dom mon dow); default 0 2 * * *
ansible_network_osGroupYesMust equal fortinet.fortios.fortios
ansible_connectionGroupYesMust equal httpapi
ansible_httpapi_use_sslGroupโ€”Boolean; must be true when port 443
ansible_httpapi_portGroupโ€”Integer 1โ€“65535; default 443
ansible_userGroupYesEnv lookup or Vault ref โ€” never a literal
ansible_passwordGroupYesEnv lookup or Vault ref โ€” never a literal secret
fortigate_backup_scopeGroupโ€”Enum: global | vdom; default global
ansible_hostHostYesValid IPv4 address or hostname
fortigate_siteHostYesNon-empty, โ‰ค120 chars
fortigate_environmentHostโ€”Enum: production | staging | lab | branch | dmz
retention_days (override)Hostโ€”Optional integer 1โ€“3650; overrides global

Example variable files

Complete, schema-valid examples for each layer. Variable precedence flows global โ†’ group โ†’ host, so per-device files only set what differs.

group_vars/all.yaml
yaml
---
# group_vars/all.yaml โ€” global defaults (Global layer)
backup_dir: "./backups"            # safe path, no shell metacharacters
retention_days: 30                 # integer 1โ€“3650
backup_timestamp: "{{ lookup('pipe', 'date +%Y-%m-%d_%H%M%S') }}"
backup_schedule: "0 2 * * *"       # 5-field cron โ€” nightly at 02:00
group_vars/fortigates/vars.yaml
yaml
---
# group_vars/fortigates/vars.yaml โ€” connection + auth (Group layer)
ansible_network_os: fortinet.fortios.fortios
ansible_connection: httpapi
ansible_httpapi_use_ssl: true      # required true when port 443
ansible_httpapi_validate_certs: false
ansible_httpapi_port: 443          # 1โ€“65535
ansible_command_timeout: 30        # 5โ€“600
ansible_httpapi_timeout: 30        # 5โ€“600

# Credentials: env lookup or Vault ref ONLY โ€” never a literal secret
ansible_user: "{{ lookup('env', 'FORTIGATE_USER') }}"
ansible_password: "{{ lookup('env', 'FORTIGATE_PASSWORD') }}"
# Vault alternative:
# ansible_user: "{{ vault_fortigate_user }}"
# ansible_password: "{{ vault_fortigate_password }}"

fortigate_backup_scope: global     # global | vdom
fortigate_api_version: v2
host_vars/fw-prod-01.yaml
yaml
---
# host_vars/fw-prod-01.yaml โ€” per-device overrides (Host layer)
ansible_host: 192.168.1.10         # valid IPv4 or hostname
fortigate_site: "Headquarters - Main Data Center"   # required, โ‰ค120 chars
fortigate_location: "Amsterdam, Netherlands"
fortigate_role: "Primary Internet Gateway"
fortigate_environment: production  # production | staging | lab | branch | dmz

# Optional overrides (fall back to group/global when omitted)
retention_days: 90                 # keep critical device backups longer
# ansible_httpapi_port: 8443
group_vars/fortigates/vault.yaml
yaml
---
# group_vars/fortigates/vault.yaml โ€” encrypted with: ansible-vault encrypt
vault_fortigate_user: "backup_svc"
vault_fortigate_password: "S3cure-Backup-Pass!"

Vault references & credential resolution

Secrets live encrypted in vault.yaml and are referenced by name from plaintext var files. At runtime Ansible resolves each reference; host overrides win over group values, and literal secrets are rejected by the schema.

vault references in vars / host_vars
yaml
---
# group_vars/fortigates/vars.yaml โ€” reference the encrypted vault values
# (vars.yaml is plaintext; only vault.yaml is encrypted)
ansible_user: "{{ vault_fortigate_user }}"
ansible_password: "{{ vault_fortigate_password }}"

# Per-device override (host_vars/fw-dmz-01.yaml) โ€” separate vault entry
ansible_user: "{{ vault_dmz_admin_user }}"
ansible_password: "{{ vault_dmz_admin_password }}"
terminal
bash
# Create / edit the encrypted vault file
ansible-vault create group_vars/fortigates/vault.yaml
ansible-vault edit   group_vars/fortigates/vault.yaml

# Run the backup, supplying the vault password
ansible-playbook forti_backup.yaml --ask-vault-pass
# ...or from a protected password file
ansible-playbook forti_backup.yaml --vault-password-file ~/.vault_pass

# Env-var alternative (no vault) โ€” schema also accepts these
export FORTIGATE_USER="backup_svc"
export FORTIGATE_PASSWORD="S3cure-Backup-Pass!"
ansible-playbook forti_backup.yaml
SourceExampleResolves to
Vault reference{{ vault_fortigate_password }}Decrypted value from group_vars/fortigates/vault.yaml at runtime
Environment lookup{{ lookup('env', 'FORTIGATE_PASSWORD') }}Value of $FORTIGATE_PASSWORD in the control-node shell
Host overridehost_vars/<device>.yaml ansible_userWins over group value for that one device (var precedence)
Literal secretansible_password: "Passw0rd!"Rejected by schema โ€” use an env lookup or Vault ref instead

Copy-ready Vault snippets

Paste the reference block into the plaintext var file and the matching block into the encrypted vault file for that layer. Use the Copy button on each block.

Global layer
group_vars/all.yaml
group_vars/all.yaml
yaml
# group_vars/all.yaml โ€” global default credentials (all FortiGates)
ansible_user: "{{ vault_global_fortigate_user }}"
ansible_password: "{{ vault_global_fortigate_password }}"
group_vars/all/vault.yaml
yaml
# group_vars/all/vault.yaml โ€” ansible-vault encrypt
vault_global_fortigate_user: "backup_svc"
vault_global_fortigate_password: "CHANGE-ME-global"
Group layer
group_vars/fortigates/vars.yaml
group_vars/fortigates/vars.yaml
yaml
# group_vars/fortigates/vars.yaml โ€” credentials for the fortigates group
ansible_user: "{{ vault_fortigate_user }}"
ansible_password: "{{ vault_fortigate_password }}"
group_vars/fortigates/vault.yaml
yaml
# group_vars/fortigates/vault.yaml โ€” ansible-vault encrypt
vault_fortigate_user: "backup_svc"
vault_fortigate_password: "CHANGE-ME-group"
Host layer
host_vars/fw-dmz-01.yaml
host_vars/fw-dmz-01.yaml
yaml
# host_vars/fw-dmz-01.yaml โ€” per-device override (wins over group)
ansible_user: "{{ vault_dmz_admin_user }}"
ansible_password: "{{ vault_dmz_admin_password }}"
host_vars/fw-dmz-01/vault.yaml
yaml
# host_vars/fw-dmz-01/vault.yaml โ€” ansible-vault encrypt
vault_dmz_admin_user: "dmz_backup_svc"
vault_dmz_admin_password: "CHANGE-ME-dmz"

Inventory

hosts.yaml
yaml
---
all:
  children:
    fortigates:
      children:
        production_fortigates:
          hosts:
            fw-prod-01: { ansible_host: 192.168.1.10 }
            fw-prod-02: { ansible_host: 192.168.1.11 }
        branch_fortigates:
          hosts:
            fw-branch-01: { ansible_host: 192.168.2.10 }
            fw-branch-02: { ansible_host: 192.168.3.10 }
        dmz_fortigates:
          hosts:
            fw-dmz-01: { ansible_host: 10.0.1.10 }

Run it

terminal
bash
# Install prerequisites
pip3 install ansible fortiosapi
ansible-galaxy collection install -r requirements.yaml

# Set credentials
export FORTIGATE_USER="admin"
export FORTIGATE_PASSWORD="YourPassword"

# Backup all devices
ansible-playbook forti_backup.yaml

# Backup a single group or device
ansible-playbook forti_backup.yaml --limit production_fortigates
ansible-playbook forti_backup.yaml --limit fw-prod-01