Provision a Samba Active Directory domain on Debian 13 "trixie", end to end: an AD domain controller and a separate, domain-joined member file/print server, with optional Proxmox VE VM creation. Managed day-to-day from Windows RSAT.
The DC and the file server are deliberately separate hosts. A member server's
vfs objects = acl_xattr / idmap config smb.conf is exactly what must never
run on a DC (it breaks samba-ad-dc), so file and print services get their own
box. Both are provisioned here.
Domain controller (domain_controllers group):
| Component | Purpose |
|---|---|
| samba-ad-dc | KDC, LDAP, AD-integrated DNS (SAMBA_INTERNAL + forwarders), SYSVOL; FL 2016, --use-rfc2307 |
| domain_seed | Tier-0/1/2 OU tree, security groups, t0-admin, tier-0 PSO, baseline password policy, empty deny-logon GPOs linked to the tier OUs, NOT_DELEGATED on the built-in Administrator |
| domain_users | End-user accounts with group membership; optional home drive and logon drive maps |
| chrony | Signed NTP (MS-SNTP) for Windows clients |
| nftables | Default-drop firewall, DC service ports only |
| samba_backup (opt-in) | Nightly samba-tool domain backup online on a systemd timer |
Member file/print server (file_servers group):
| Component | Purpose |
|---|---|
| samba (member) | security = ADS, winbind, rid idmap, acl_xattr NT ACLs |
| samba_shares | Data-driven SMB shares; SeDiskOperatorPrivilege to a storage-admin group; optional [homes] |
| samba_printers (opt-in) | CUPS print sharing — driverless IPP (default) or legacy print$ |
| nftables | Default-drop firewall, SMB (+ IPP) only |
Additional DCs (additional_dcs group) join for HA and pull SYSVOL from the PDC
emulator over an SSH forced-command rsync on a timer.
| Role | Does |
|---|---|
proxmox_vm |
Clones a Debian 13 cloud-init template over the API; idempotent on VMID, never destroys |
os_base |
Hostname, /etc/hosts, resolv.conf, base packages; asserts a static IP |
time_sync |
chrony — signed-NTP server on the DC, client on members |
samba_ad_dc |
Provisions the first DC |
domain_seed |
OU / group / admin / PSO / GPO model |
domain_users |
End-user accounts, home drives, drive maps |
samba_dc_join |
Joins an additional DC and bootstraps SYSVOL sync |
samba_member_join |
Joins the member server (security = ADS) |
samba_shares |
NT-ACL file shares and [homes] |
samba_printers |
CUPS / Samba print sharing |
acidnetworks.base_debian.base_firewall |
nftables host firewall (external) — SSH via mgmt tier, AD/SMB ports via the service-CIDR tier |
samba_backup |
DC online-backup timer |
ansible-galaxy collection install -r requirements.ymlcommunity.general; community.proxmox is only needed if the repo creates the VMs.
Keys are gitignored — generate your own. The public half is injected into VMs created on Proxmox.
ssh-keygen -t ed25519 -f ~/.ssh/samba-ad-deploy -N "" -C "samba-ad-deploy"
cp ~/.ssh/samba-ad-deploy.pub files/ssh/deploy.pubEdit inventory/group_vars/all/vault.yml (a plaintext stub) with real values,
then encrypt it:
ansible-vault encrypt inventory/group_vars/all/vault.ymlSecrets: vault_administrator_password, vault_tier0_admin_password,
vault_join_password, vault_user_initial_password, and — for Proxmox —
vault_pve_api_token_secret.
Set the realm/NetBIOS, hosts and IPs. inventory/group_vars/all/main.yml holds
the realm, the OU/group model, users, and drive maps; group_vars/file_servers.yml
holds the share definitions. Put production hosts in inventory/hosts.yml, or use
inventory/proxmox.yml to create them (below).
ansible-playbook playbooks/provision-dc.yml --ask-vault-pass
ansible-playbook playbooks/provision-fileserver.yml --ask-vault-passDay-2 and HA:
ansible-playbook playbooks/join-dc.yml --ask-vault-pass # add an HA DC + SYSVOL sync
ansible-playbook playbooks/shares.yml --ask-vault-pass # change file shares onlysite.yml clones the VMs from the cloud-init template pve_template_id, then
provisions the DC and member. Create an API token (e.g. root@pam!ansible) with
VM create/clone/config rights, put the secret in the vault, set the API
host/node/storage and the per-host VMIDs/IPs in inventory/proxmox.yml, then:
ansible-playbook -i inventory/proxmox.yml site.yml --ask-vault-passRe-runs are idempotent; an existing VMID is left untouched. Teardown is manual
(qm destroy <vmid>). A run that fails after the clone leaves a half-configured
VM that a re-run will skip — qm destroy that VMID and re-run.
Two things can't be automated and need a Windows box with RSAT:
- Deny-logon URA GPOs — the role creates and links empty tier GPOs;
samba-toolcan't write User Rights Assignment, so author the "Deny log on …" entries in GPME. Never link a deny-logon GPO at the domain root. - Printer install (
print$mode only) — pre-stage drivers; don't weaken Windows Point-and-Print hardening to make it work.
Point the client's DNS at the DC and its DNS suffix at the realm first.
On the DC:
sudo samba-tool domain info 127.0.0.1
sudo samba-tool domain level show # 2016 / 2016
host -t SRV _ldap._tcp.<realm> 127.0.0.1On the member:
sudo net ads testjoin # Join is OK
wbinfo -u | head # domain users resolve
smbclient -L //localhost -U administrator # shares listedTwo Debian 13 boxes (dc, fs) on a private network with a throwaway realm and
plaintext test passwords — no vault. Run each playbook twice; the second run must
report changed=0.
vagrant up
ansible-galaxy collection install -r requirements.yml # base_debian + hardening_debian
ansible-playbook -i inventory/vagrant.yml playbooks/provision-dc.yml
ansible-playbook -i inventory/vagrant.yml playbooks/provision-dc.yml # changed=0
ansible-playbook -i inventory/vagrant.yml playbooks/provision-fileserver.yml
ansible-playbook -i inventory/vagrant.yml playbooks/provision-fileserver.yml # changed=0
# Hardening layer (what site.yml runs last in prod), over dc + fs:
ansible-playbook -i inventory/vagrant.yml acidnetworks.hardening_debian.harden
ssh vagrant@10.99.0.10 # DC must still answer
vagrant destroy -fPer-role test playbooks live in tests/ (tests/test-<role>.yml).
- Ansible core ≥ 2.16 with
community.general(pluscommunity.proxmoxto create the VMs). - Managed hosts: fresh Debian 13 with a static IP and clocks within Kerberos
tolerance;
os_baseasserts the static IP and does not configure NICs. - An owned subdomain for the realm (e.g.
ad.example.com) — never a.localrealm. - Run from the repo root. If your shell exports
ANSIBLE_ROLES_PATHorANSIBLE_COLLECTIONS_PATH, unset them soansible.cfgtakes effect.