Group Policy Objects are at the heart of any robust Active Directory infrastructure. They define security settings, permissions, device configurations, and software restrictions. In the event of a disaster, human error, or simply a need to roll back after a change, not having a backup of your GPOs means facing hours of tedious reconstruction.
The graphical interface of the Group Policy Management Console does offer a backup feature, but it is manual, easy to overlook, and, most importantly, not versioned. The goal of this article is to automate this process cleanly using Ansible via WinRM, on an infrastructure comprising two separate Active Directory domains, with long-term archiving on a NAS and reporting via email.
Target Architecture
The infrastructure in question is based on:
- Two independent AD domains, each with its own primary domain controller (PDC)
- A Linux Ansible server that manages the entire infrastructure via scheduled playbooks
- A TrueNAS Scale NAS serving as a long-term archive
- An internal SMTP relay for notifications
The backup process consists of several steps:
- HTTPS WinRM connection (port 5986) on each PDC
- Execution of
Backup-GPOandGet-GPOReportin PowerShell - Transfer of files to the Ansible server
- Transfer to the NAS via rsync over SSH
- Deletion of backups older than one year
- Sending an HTML report via email
Prerequisites
Windows Side
WinRM must be enabled on the domain controllers. To verify:
winrm enumerate winrm/config/listener
If no listener is configured:
winrm quickconfig -quiet
The PowerShell GroupPolicy module must be present—it is installed natively on any server promoted to a domain controller.
Ansible service accounts must have Group Policy Creator Owners or Domain Admins permissions to run Backup-GPO.
Ansible Side
The pywinrm package must be installed on the Ansible server:
pip install pywinrm --break-system-packages
Service account passwords are stored in a dedicated Ansible Vault and are never transmitted in plain text.
The inventory
The inventory defines the two PDCs with their domain-specific WinRM connection settings. Note the use of ansible_winrm_scheme: https and port 5986 — WinRM over plain HTTP on port 5985 is not acceptable in a production environment, even an internal one.
---
all:
children:
pdc_domain1:
hosts:
PDC-DOM1:
ansible_host: 192.168.X.X
ansible_user: "domain1\\ansible_svc"
ansible_password: "{{ vault_ansible_password_domain1 }}"
ansible_connection: winrm
ansible_winrm_transport: ntlm
ansible_winrm_port: 5986
ansible_winrm_scheme: https
ansible_winrm_server_cert_validation: ignore
domain_name: domain1.local
pdc_domain2:
hosts:
PDC-DOM2:
ansible_host: 192.168.X.X
ansible_user: "domain2\\ansible_svc"
ansible_password: "{{ vault_ansible_password_domain2 }}"
ansible_connection: winrm
ansible_winrm_transport: ntlm
ansible_winrm_port: 5986
ansible_winrm_scheme: https
ansible_winrm_server_cert_validation: ignore
domain_name: domain2.lan
The domain_name variable is a custom host variable—it will be reused in the playbook to name backup folders and customize log messages.
The Playbook
Play 1 — Backup on the PDCs
The first play runs on both host groups in parallel. Two PowerShell commands are executed:
Backup-GPO -Allcreates individual backups in subfolders identified by GUID — this is the native format, which can be restored directly usingRestore-GPOGet-GPOReport -ReportType XMLgenerates a consolidated XML report of all GPOs in the domain, with their detailed settings — this file will be useful for future analysis
- name: "{{ domain_name }} - Back up all GPOs"
ansible.windows.win_shell: |
Import-Module GroupPolicy
Backup-GPO -All -Path "C:\GPO_Backups\{{ domain_name }}"
Get-GPOReport -All -ReportType XML `
-Path "C:\GPO_Backups\{{ domain_name }}\all_gpo_report_{{ domain_name }}.xml"
register: gpo_result
The files are then transferred back to the Ansible server using the fetch module, into a dated temporary directory.
Play 1 — Transfer to the NAS
The transfer uses rsync via SSH with sshpass for password authentication (a dedicated SSH key would be more elegant, but the constraints of the TrueNAS Scale environment led to this solution — a topic for a future article). The --no-perms --no-owner --no-group flags prevent permission errors related to differences between Linux and ZFS file systems.
- name: "{{ domain_name }} - Copy to NAS via rsync"
delegate_to: localhost
shell: >
SSHPASS='{{ vault_password_nas }}'
sshpass -e rsync -avz --no-perms --no-owner --no-group --mkpath
-e "ssh -p PORT_SSH -o StrictHostKeyChecking=no"
/tmp/gpo_backups/{{ domain_name }}/{{ date_suffix }}/
user@nas-host:/mnt/ARCHIVE/DC_BACKUP/{{ domain_name }}/{{ date_suffix }}/
no_log: true
Files older than 365 days are purged directly on the NAS via SSH:
- name: "{{ domain_name }} - Purge backups older than 365 days"
delegate_to: localhost
shell: >
SSHPASS='{{ vault_password_nas }}'
sshpass -e ssh -p PORT_SSH -o StrictHostKeyChecking=no user@nas-host
"find /mnt/ARCHIVE/DC_BACKUP/{{ domain_name }} -maxdepth 1 -type d -mtime +365 -exec rm -rf {} \;"
ignore_errors: true
no_log: true
Play 2 — HTML Report
A second play runs on localhost after the backups. It generates an HTML report by accessing the variables stored on each host via hostvars, then sends it via the internal SMTP relay.
The report displays the PowerShell backup status and the NAS transfer status for each domain, with visual indicators ✅ / 🔴 for quick reference.
- name: GPO Backup Report
hosts: localhost
gather_facts: yes
tasks:
- name: Send report via email
ansible.builtin.mail:
host: smtp.domain.local
port: 25
to: "admin@domaine.local"
from: "ansible@serveur.domaine.local"
subject: "GPO Backup Report - {{ ansible_date_time.date }}"
body: "{{ recap_message }}"
secure: never
subtype: html
Backup Structure on the NAS
/mnt/ARCHIVE/DC_BACKUP/
├── domain1.local/
│ ├── 2026-03-31/
│ │ ├── all_gpo_report_domain1.local.xml ← consolidated report
│ │ ├── {GUID-1}/ ← individual GPO backup
│ │ │ ├── Backup.xml
│ │ │ ├── bkupInfo.xml
│ │ │ └── DomainSysvol/
│ │ └── {GUID-2}/
│ └── 2026-04-07/
└── domain2.lan/
└── 2026-04-07/
Each GUID folder corresponds to an individual GPO, in the native Windows format—ready for use with Restore-GPO or the GPMC console without any prior manipulation.
Scheduling
The playbook is scheduled via cron every Monday morning, in line with the infrastructure’s other Ansible maintenance tasks (updates, network backups, NAS backups):
# GPO Backup — every Monday at 8:45 AM
45 8 * * 1 ansible-playbook /etc/ansible/playbooks/gpo-backup.yml \
-i /etc/ansible/inventory-windows.yml \
--vault-password-file /etc/ansible/vault/.vault_pass \
-e "@/etc/ansible/vault/network-secrets.yml" \
>> /var/log/ansible-gpo-backup.log 2>&1
What this backup does — and what it doesn’t do
This playbook produces backups that are natively restorable using Restore-GPO. In the event of accidental deletion of a GPO or incorrect modification, you can revert to the state of the last backup in just a few minutes.
The all_gpo_report_*.xml file offers additional value: it contains all the settings for each GPO in a structured XML format. This file enables qualitative analysis—detecting redundancies, obsolete settings, and empty or unlinked GPOs. We’ll cover this topic in a future article: loading this file into an analysis tool to audit the consistency of an AD domain.
What this playbook does not cover, however, is backing up SYSVOL (startup scripts, custom ADMX templates), which requires a complementary approach. For a complete recovery in the event of a major disaster, Veeam or an equivalent solution takes over for the entire DCs.
Conclusion
Automating GPO backups with Ansible and WinRM turns out to be fairly straightforward, provided you pay attention to three key points: WinRM authentication over HTTPS (not plain HTTP), proper naming of vault variables for service accounts, and managing PowerShell permissions on the Windows side.
The benefits of this approach over manual GUI backups are threefold: backups run regularly without human intervention, they are versioned by date on dedicated storage with long-term retention, and they integrate into the existing reporting workflow—a consolidated weekly email that provides an overview of the health of the infrastructure’s backups.
The next logical step is to analyze the contents of these backups. The XML report generated by Get-GPOReport is a goldmine of information—it’s just waiting to be leveraged.