ilkermanap f6b241b3f7
All checks were successful
build-image / build (push) Successful in 5m41s
Force IPv4 in the build VM, where apt was losing 31s per large fetch
Every apt download over roughly 15 MB inside the build VM took exactly 31
seconds regardless of its size, while a 14.1 MB one took 1 second at 23 MB/s.
A fixed cost that ignores size is a timeout, not a bandwidth limit: QEMU's
user-mode network advertises IPv6 that does not work, so apt's parallel
connections black-holed on it and only fell back to IPv4 when the 30-second
timeout expired.

Patch the build VM's cloud-init seed from bootcmd, which runs in the
init-local stage before SSH is up, so the setting covers upstream's apt calls
as well as ours. The same 28.5 MB fetch went from 31s (914 kB/s) to 3s
(9152 kB/s).

Also fix APT_PROXY, which never worked as documented. A cache cannot see
inside a CONNECT tunnel, so the repositories have to be rewritten to plain
http first, and Debian 13 keeps the real mirror URLs in /etc/apt/mirrors/*.list
behind the mirror+file: method, which rewriting sources.list alone misses.

The README now carries the measurements rather than estimates. The headline is
that the cache barely matters: with it fully warm it saves about three seconds
of a 4m40s build, so a local Debian mirror (138 GB for trixie amd64) would buy
nothing here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:59:39 +02:00

maas-samba-ad

Build a Debian image that MAAS can deploy to bare metal as an Active Directory domain controller, with first-boot automation that either creates a new domain or joins an existing one — without anyone logging in.

The domain controller is Samba in AD DC mode. To a Windows client it is an Active Directory domain: same Kerberos, same LDAP, same Group Policy, same net use, same domain join.

Verified end to end on real infrastructure: two domain controllers deployed from MAAS, replicating, with a Windows Server 2025 machine joined to the domain. See Verified status for exactly what was and was not tested.


Table of contents


If Active Directory is new to you

Skip this if it isn't.

There is no "primary" and "backup" domain controller. That is Windows NT 4 terminology, retired in 2000. In Active Directory every DC holds a full, writable copy of the directory and they replicate to each other. Any DC can service any logon.

What does live on exactly one DC at a time are the five FSMO roles — small coordination duties such as allocating blocks of security identifiers. One of them is called "PDC Emulator", which is where the old name survives and where most of the confusion comes from. Losing the DC that holds them does not cost you the domain; you seize the roles onto another DC and carry on.

So this image has two modes, and they map to the real distinction:

Mode What it does
AD_MODE=provision Creates the domain. The first DC, and initially the FSMO holder
AD_MODE=join Joins the existing domain as another equal DC

A domain is not one service. These have to work together, and all of them except the clock come from the single samba daemon:

Piece Why it matters
LDAP The directory itself: users, groups, computers, policy links
Kerberos Issues the tickets clients actually authenticate with
DNS Clients find domain controllers through SRV records. AD without working DNS does not function at all — this is the single most common cause of a broken domain
SMB Serves SYSVOL and NETLOGON, the shares Group Policy and logon scripts live in
Time Kerberos refuses tickets when clocks differ by more than five minutes. The symptom is logins failing for no visible reason

The image installs and wires up all of them.

Names you have to choose. Two, and they are awkward to change later:

  • Realm — your DNS domain in uppercase, e.g. AD.EXAMPLE.COM. Use a domain you control. Do not use your public web domain, and never a bare .local (it collides with mDNS).
  • NetBIOS name — the short legacy form, e.g. EXAMPLE. Uppercase, at most 15 characters, no dots. Conventionally the realm's first label.

Passwords. Active Directory enforces complexity by default: at least seven characters, and three of upper case, lower case, digit, symbol. A weak AD_ADMIN_PASSWORD makes provisioning fail with an error that does not say so.


What "highly available" means here

Availability in an AD domain comes from having more than one DC, not from clustering anything. Concretely:

  1. At least two DCs. Two survives losing one. Three is better, because with two you have no majority when one is down and some operations get cautious.
  2. Each DC runs DNS, serving the same AD-integrated zones. This image does that automatically — every DC answers for the domain.
  3. Clients must be told about both. Hand out both DC addresses as DNS servers over DHCP. A client that only knows one DC has no redundancy no matter how many you run. In MAAS that is the subnet's DNS server list.
  4. Time comes from the DCs. Set AD_NTP_ALLOW so members can use them.
  5. SYSVOL has to be replicated. This is the part that does not happen by itself — see below.

The FSMO roles sit on the first DC. If it dies permanently you seize them; the domain keeps authenticating in the meantime.


The SYSVOL problem

Read this before relying on the result.

SYSVOL is the share holding Group Policy objects and logon scripts. Windows replicates it between DCs using DFS-R. Samba implements neither DFS-R nor its predecessor FRS, so a policy created on one DC never reaches the others by itself. Clients then behave differently depending on which DC happened to answer them — and nothing reports an error.

The Samba wiki's workaround is to copy SYSVOL with rsync and then reapply the ACLs from AD. This image ships that as adc-sysvol-sync, run by a systemd timer:

  1. Sync idmap.ldb from the source DC once. Without it the same file shows different ownership on different DCs, because the SID-to-uid mapping differs.
  2. rsync -aAX --delete the SYSVOL tree.
  3. samba-tool ntacl sysvolreset, because rsync carries POSIX bits while the Windows ACLs live in AD and have to be reapplied — skip it and clients get access-denied on Group Policy.

It needs a root SSH key on the joining DC that is authorised on the source DC. If there isn't one, the sync exits with an explanation instead of pretending to work. That is deliberate: silent SYSVOL divergence is worse than a visible failure.

This is a real limitation of Samba, not of this image. If you need genuine multi-master SYSVOL replication, you need Windows DCs.


What Samba does not do

Two gaps matter in practice. Both are Samba's, not this image's.

No ADWS. Samba does not implement Active Directory Web Services (TCP 9389), so the PowerShell ActiveDirectory module — Get-ADUser, Get-ADDomain, Get-ADDomainControllerdoes not work against a Samba DC. It fails with "Unable to find a default server with Active Directory Web Services running."

What does work is everything built on LDAP and RPC, which is most of it:

  • ADUC, ADSI Edit and the rest of the MMC snap-ins
  • Raw LDAP from PowerShell (DirectoryServices.DirectoryEntry), dsquery, net
  • samba-tool on the DCs themselves

So you manage the domain with the graphical tools or with LDAP, not with the AD PowerShell cmdlets. Third-party ADWS implementations for Samba exist; none is shipped here.

No DFS-R for SYSVOL. Covered in The SYSVOL problem.

How it works

Debian 13 cloud image (qcow2, official)
   │
   ├─ canonical/packer-maas, "debian" template   (QEMU + KVM)
   │     ├─ cloud-init / netplan / curtin compatibility      [upstream]
   │     └─ customize-samba-ad.sh                            [this repo]
   │           ├─ samba, samba-ad-dc, winbind, krb5, chrony, rsync
   │           ├─ swap the cloud kernel for the generic one
   │           ├─ delete every trace of a domain
   │           ├─ mask smbd/nmbd/winbind, disable networking.service
   │           └─ install the overlay (adc-maas-init, sysvol sync, curtin-hooks)
   │
   └─ samba-ad-dc.tar.gz   ──►   maas boot-resources create

On first boot adc-maas-init runs these stages, each once, recorded in /var/lib/adc-maas/<stage>.done:

Stage What it does
hosts Makes the FQDN resolve to the management address. Samba insists on this
identity Regenerates anything that must not be shared between clones
time Configures chrony, including the signed-NTP socket Windows clients expect
resolver Points DNS at the DC being joined, then at itself once it serves DNS
domain samba-tool domain provision or samba-tool domain join
services Masks the standalone file-server daemons, starts samba-ad-dc
sysvol Sets up SYSVOL replication where it is needed
selftest Proves Kerberos issues a ticket and SMB answers

A stage that fails leaves the service failed and is retried on the next boot, rather than leaving a half-configured domain controller that looks fine.


Requirements

Build host: Ubuntu 22.04+ with access to /dev/kvm, 4+ vCPU, 8+ GB RAM, 25+ GB free. If it is a VM, nested virtualization must be on and the CPU type must pass the flags through (on Proxmox: --cpu host).

Deployment: MAAS 3.2+ with the curtin preseed from this repository installed on the region controller.


Quick start

sudo ./scripts/install-deps.sh     # packer, qemu, ovmf, nbdkit, fuse2fs
sudo make image                    # -> build/samba-ad-dc.tar.gz
make verify
make preseed
sudo make install-preseed          # onto the MAAS region controller
make upload MAAS_PROFILE=admin

The preseed is not optional. Without it the deployment fails — see Traps.


Deploying a domain

The first DC

maas $PROFILE machine deploy $SYSTEM_ID \
    osystem=custom distro_series=samba-ad-dc \
    user_data="$(base64 -w0 maas/examples/01-first-dc.yaml)"

with, in that user-data:

AD_MODE=provision
AD_REALM=AD.EXAMPLE.COM
AD_DOMAIN=EXAMPLE
AD_ADMIN_PASSWORD='...'
AD_DNS_FORWARDER=192.0.2.1
AD_NTP_ALLOW=192.0.2.0/24

Every DC after that

AD_MODE=join
AD_REALM=AD.EXAMPLE.COM
AD_DOMAIN=EXAMPLE
AD_JOIN_PEER=192.0.2.10          # an existing DC
AD_ADMIN_PASSWORD='...'          # a Domain Admin on it
AD_SYSVOL_SYNC=on
AD_SYSVOL_SOURCE=192.0.2.10

Full examples: maas/examples/.

Then

Point the subnet's DHCP at both DCs for DNS, and check the domain from either:

samba-tool domain level show
samba-tool drs showrepl          # replication between DCs
samba-tool fsmo show             # who holds the five roles

Security notes

  • AD_ADMIN_PASSWORD is plaintext in MAAS user-data, where anyone with MAAS access can read it. Use a short-lived password and change it after the domain is up. It is scrubbed from conf.d on the node once the domain is running (AD_WIPE_SECRETS=true), but not from MAAS.
  • The SYSVOL SSH key, if you supply one through user-data, has the same exposure. Use a key dedicated to that job and authorised for nothing else.

Configuration reference

Defaults live in /etc/adc-maas/adc-maas.conf, which documents every option. Per-node settings go in /etc/adc-maas/conf.d/*.conf, written by cloud-init from the user-data MAAS supplies, and override the defaults.

Option Default Meaning
AD_MODE none none, provision or join
AD_REALM (empty) Kerberos realm, uppercase DNS domain
AD_DOMAIN (empty) NetBIOS name, uppercase, ≤15 chars
AD_ADMIN_PASSWORD (empty) Administrator password / join credentials
AD_ADMIN_PASSWORD_FILE (empty) Read it from a file instead
AD_JOIN_USER Administrator Account used to join
AD_JOIN_PEER (empty) An existing DC to join
AD_DNS_BACKEND SAMBA_INTERNAL Or BIND9_DLZ if you need BIND's features
AD_DNS_FORWARDER (empty) Where to send queries the DC is not authoritative for
AD_FUNCTION_LEVEL 2008_R2 Domain and forest functional level
AD_SITE Default-First-Site-Name AD site to join
AD_USE_RFC2307 true Store POSIX uid/gid in AD
AD_INTERFACE (auto) Interface Samba binds to
AD_NTP_ALLOW (empty) Subnet allowed to use this DC as a time source
AD_SYSVOL_SYNC auto auto (on for joined DCs), on, off
AD_SYSVOL_SOURCE (join peer) DC to pull SYSVOL from
AD_SYSVOL_INTERVAL 5min How often
AD_WAIT / AD_RETRIES 900 / 5 Waiting for the peer, and join attempts
AD_WIPE_SECRETS true Scrub the password from conf.d afterwards
AD_ENABLED true Set false to disable all first-boot automation

Operating the domain

# Users and groups
samba-tool user create alice
samba-tool group addmembers "Domain Admins" alice

# Health
samba-tool drs showrepl                  # is replication working?
samba-tool dns query localhost <realm> @ ALL -U Administrator

# Which DC holds the FSMO roles
samba-tool fsmo show

# The first DC is gone for good: take the roles onto this one
samba-tool fsmo seize --role=all

# A DC that will never come back has to be removed from the directory,
# or replication keeps trying to reach it
samba-tool domain demote --remove-other-dead-server=<name>

Windows clients join the domain exactly as they would against a Windows DC, provided their DNS points at a DC.


Build configuration

Variable Default Meaning
DEBIAN_SERIES / DEBIAN_VERSION trixie / 13 Debian release
AD_EXTRA_PACKAGES acl attr ldap-utils … Extra packages baked in
IMAGE_NAME samba-ad-dc MAAS name and preseed filename
ARCH / BOOT amd64 / uefi Architecture and boot mode
DISK_SIZE 16G Build VM disk; upstream's 4G is too small
PM_REF pinned SHA canonical/packer-maas revision
APT_PROXY (empty) Local APT cache, e.g. http://10.0.2.2:3142 — see make deps-cache

make check-upstream compares the samba version in Debian against the image you have, without building anything.

Build performance

A full build takes about 4m40s on a 4 vCPU / 4 GB build VM. Where that time goes was measured rather than guessed, and the result is not what it looked like.

The obvious suspect was slow repository access: inside the build VM apt reported 600-900 kB/s, while the build host itself pulled from deb.debian.org at 48 MB/s. But the pattern gave it away — every fetch over roughly 15 MB took exactly 31 seconds no matter how big it was, while a 14.1 MB fetch took 1 second at 23 MB/s. That is a connection timeout, not a bandwidth limit. QEMU's user-mode network offers IPv6 that does not actually work, so apt's parallel connections black-holed on it and only fell back to IPv4 after 30 seconds.

The Makefile now patches the build VM's cloud-init seed to write Acquire::ForceIPv4 "true" from bootcmd, which runs before SSH is up and therefore covers upstream's own apt calls as well as ours. The same 28.5 MB fetch, across three builds:

Build Setup Time Rate
1 no cache, no patch 31 s 914 kB/s
2 apt-cacher-ng, no patch 31 s 916 kB/s
3 apt-cacher-ng + ForceIPv4 3 s 9152 kB/s

That fetch goes over https, which a cache passes through a CONNECT tunnel without storing, so build 2 isolates the cache from the patch: the cache changed nothing, the one-line apt setting was worth 28 seconds.

A local APT cache is still supported and does help on repeat builds, just far less than you would expect:

sudo make deps-cache                              # installs apt-cacher-ng
sudo make image APT_PROXY=http://10.0.2.2:3142

With a fully warm cache the 26.2 MB Samba fetch went from 2s to 0s (59 MB/s) and the 17.9 MB fetch from 1s to 0s — about three seconds off a 4m40s build. The rest of the time is qemu, dpkg and image compression, none of which a faster mirror touches. A full Debian trixie amd64 mirror costs about 138 GB; the cache that produced these numbers is 44 MB. Mirror the archive if you want it for other reasons, but not to speed these builds up.

10.0.2.2 is the build host as seen from Packer's user-mode network. When a proxy is configured, repositories are rewritten from https to http so the cache can serve them; package signatures are still verified. Debian 13 keeps the real mirror URLs in /etc/apt/mirrors/*.list behind the mirror+file: method, so rewriting sources.list alone is not enough.


Traps this image works around

These are the same class of problem as in the sibling maas-proxmox project, and each fails silently:

  1. kernel: null in the preseed. Some curtin versions shipped with MAAS crash on it. The image carries /curtin/curtin-hooks instead, which disables the kernel install and works regardless of curtin version.
  2. Interface renaming. MAAS records the interface name it saw in its Ubuntu commissioning environment; Debian's udev may name the same card differently, and cloud-init then fails to rename it and leaves the link down. curtin-hooks pins MAC-to-name mappings so udev gets it right from the start.
  3. systemd ordering. Ordering the first-boot unit after cloud-final.service while it is WantedBy=multi-user.target forms a cycle, and systemd resolves it by deleting the unit's start job — it never runs and reports nothing. The unit is Type=simple with no cloud-init ordering; the script waits for cloud-init itself.
  4. networking.service. ifupdown starting with the build VM's stale interface definition takes the real interface down before any automation runs. It ships disabled, and MAAS owns the network.

One was found by testing rather than reading:

  1. A sync service that killed itself. adc-sysvol-sync.service originally declared Requires=samba-ad-dc.service. The sync restarts samba-ad-dc after copying idmap.ldb, and systemd stops units that require a service being restarted — so the sync died mid-run with SIGTERM. The timer restarted it and it succeeded, so the outcome looked fine while the mechanism was broken. It is Wants= now, and the restart is --no-block.

Two more are specific to this image:

  1. The cloud kernel. The Debian cloud image ships linux-image-cloud-amd64, built for virtual machines and missing most physical-hardware drivers. A bare-metal DC deployed with it can come up with no disk or no network. The build swaps in the generic kernel.
  2. A domain baked into the image. Installing the packages leaves a default smb.conf. If that shipped, every machine from the image would start from the same half-configured directory and samba-tool domain provision would refuse to run. The build deletes all of it.

Verified status

Tested end to end on real infrastructure. Be sceptical of anything not listed under Verified.

Test environment

MAAS 3.7.2 (snap), isolated subnet, MAAS as gateway and DHCP
DCs 2 x (2 vCPU, 6 GB, 32 GiB, UEFI), deployed from this image
Windows client Windows Server 2025 Standard Evaluation, unattended install
Realm AD.MAASTEST.LAN / NetBIOS MAASTEST, functional level 2008 R2
Image samba 4.22.10, Debian 13, kernel 6.12.107

Verified

Area Evidence
Build 28/28 checks in make verify; 412 MB image
Build speed Four builds measured; ForceIPv4 took the 28.5 MB fetch from 31 s to 3 s, a warm APT cache saved a further ~3 s of 4m40s
Release pipeline Published to a release, downloaded anonymously, SHA-256 matched, uploaded to MAAS
Deployment Both DCs reach Deployed from custom/samba-ad-dc
Provisioning First DC created the domain in 21 s, unattended, first attempt
Joining Second DC joined in 23 s, first attempt
Kerberos The built-in self-test obtained a ticket for Administrator@AD.MAASTEST.LAN on both DCs
Replication samba-tool drs showrepl: outbound neighbours, last attempt successful, 0 consecutive failures
FSMO All five roles on the first DC, as expected
SYSVOL replication A file written on DC1 appeared on DC2 within one timer interval; log shows the idmap.ldb sync, the rsync and ntacl sysvolreset
Credential scrubbing AD_ADMIN_PASSWORD removed from conf.d after provisioning on both DCs
SMB shares sysvol and netlogon served; contents readable with domain credentials
Windows DC discovery _ldap._tcp.dc._msdcs.ad.maastest.lan resolved to maas-node8:389
Windows domain join Add-Computer succeeded; after reboot PartOfDomain=True, Test-ComputerSecureChannel=True
DC capability flags nltest /dsgetdc reports PDC GC DS LDAP KDC TIMESERV GTIMESERV WRITABLE DNS_DC DNS_DOMAIN DNS_FOREST FULL_SECRET
Group Policy gpupdate /force /target:computer completed successfully; gpresult shows the domain and site
SYSVOL from Windows Readable over UNC with domain credentials, including the replicated test file
LDAP from Windows DirectoryServices.DirectoryEntry listed both DCs and the Windows machine account, and the domain users

Not verified

  • Windows client OS (10/11) joining — only Windows Server 2025 was tested
  • Applying an actual Group Policy object and observing its effect; only the GPO refresh mechanism was exercised
  • Interactive domain logon at the Windows console, and ADUC's GUI. LDAP was proven with explicit credentials, which is the path ADUC uses, but the MMC snap-in itself was not opened
  • Losing a DC and seizing FSMO roles
  • More than two DCs, and concurrent joins
  • BIND9_DLZ — only SAMBA_INTERNAL DNS was used
  • AD_SYSVOL_SYNC without an SSH key, i.e. the refuse-and-explain path
  • Real bare metal — both DCs were virtual machines
  • arm64
  • Domain trusts, and interop with a Windows DC in the same forest

Known not to work

  • ADWS: port 9389 is closed, confirmed from the Windows client. The PowerShell ActiveDirectory module cannot be used. See What Samba does not do.

Licensing

AGPL-3.0-or-later, see LICENSE.

The build pipeline derives from canonical/packer-maas (AGPLv3) — the curtin preseed and curtin-hooks in particular — so the copyleft carries over. The upstream template is cloned at build time, not vendored.

Samba itself is GPLv3+ and is installed from Debian, unmodified.

Description
MAAS-deployable Samba Active Directory domain controller image
Readme AGPL-3.0 138 KiB
Languages
Shell 75.9%
Makefile 18.1%
Python 6%