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>
Deploying the image found a real defect. adc-sysvol-sync.service declared
Requires=samba-ad-dc.service, and the sync restarts samba-ad-dc after copying
idmap.ldb. systemd stops units that *require* a service being restarted, so the
sync killed itself with SIGTERM part way through:
adc-sysvol-sync[2065]: syncing idmap.ldb from 10.10.10.22 (one time)
adc-sysvol-sync.service: Main process exited, code=killed, status=15/TERM
adc-sysvol-sync.service: Failed with result 'signal'
The timer started it again and the second run succeeded, so the end state looked
correct while the mechanism was broken — the kind of fault that stays hidden
until it matters. The dependency is Wants= now, which gives the ordering without
the stop propagation, and the restart is --no-block.
The image was tested end to end: two DCs deployed from MAAS, the first
provisioning the domain in 21 seconds and the second joining in 23, both
unattended and on the first attempt. DRS replication is healthy, the FSMO roles
sit where they should, and a file written into SYSVOL on one DC reached the other
within a timer interval, ACLs reset.
A Windows Server 2025 machine then joined the domain: it found the DC through the
_ldap._tcp.dc._msdcs SRV record, Add-Computer succeeded, and after rebooting the
secure channel tested good. nltest reports the DC with the full set of
capabilities Windows expects — PDC GC DS LDAP KDC TIMESERV GTIMESERV WRITABLE
DNS_DC DNS_DOMAIN DNS_FOREST FULL_SECRET. Group Policy refresh completed, and
LDAP queries from the client listed both DCs, the Windows machine account and the
domain users.
One genuine limitation surfaced and now has its own section: Samba does not
implement ADWS, so port 9389 is closed and the PowerShell ActiveDirectory module
cannot be used against these DCs. ADUC, ADSI and raw LDAP are unaffected, which
is what the LDAP queries above demonstrate.
Verified status now separates what was observed from what was not. Notably still
untested: applying an actual GPO, an interactive domain logon, seizing FSMO roles
after losing a DC, BIND9_DLZ, and real bare metal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Builds a Debian image that MAAS deploys to bare metal as an Active Directory
domain controller, with first-boot automation that either creates a domain or
joins an existing one without anyone logging in. To a Windows client the result
is an AD domain: same Kerberos, same LDAP, same Group Policy, same domain join.
The request was for "primary and backup domain controllers", which is NT 4
terminology. Active Directory has no such split — every DC holds a writable copy
and any of them can service a logon. What lives on one DC at a time are the five
FSMO roles, one confusingly named "PDC Emulator". The image therefore offers
provision (create the domain) and join (add another equal DC), which is the
distinction that actually exists.
Contents:
* customize-samba-ad.sh, run inside the packer-maas build VM: installs samba,
winbind, Kerberos, chrony and rsync, swaps the cloud kernel for the generic
one, and deletes every trace of a domain so one image can produce many DCs
* adc-maas-init, a first-boot state machine covering /etc/hosts, node-unique
identifiers, time, the resolver, provisioning or joining, the service
switchover and a self-test that proves Kerberos issues a ticket
* adc-sysvol-sync, a systemd timer implementing the SYSVOL workaround
* a MAAS curtin preseed, cloud-init examples, and the release pipeline
Two findings shaped the design.
Debian builds Samba against its bundled Heimdal rather than system MIT Kerberos
— samba-ad-dc does not depend on krb5-kdc — so the AD DC role is in Samba's
supported configuration, not the experimental MIT one. The build asserts this
and fails if it ever changes.
Samba implements neither DFS-R nor FRS, so SYSVOL — where Group Policy lives —
does not replicate between DCs. Left alone, a policy created on one DC never
reaches the others and clients behave differently depending on which DC answered
them, with nothing reporting an error. adc-sysvol-sync applies the Samba wiki's
rsync workaround: sync idmap.ldb once so SID-to-uid mappings agree, rsync the
tree, then samba-tool ntacl sysvolreset because rsync carries POSIX bits while
the Windows ACLs live in AD. Without an SSH key to the source DC it exits with
an explanation rather than letting Group Policy diverge quietly.
Also carried over from maas-proxmox, where they were verified on real hardware:
the curtin-hooks that skip the kernel install and pin interface names by MAC,
Type=simple on the first-boot unit to avoid the systemd ordering cycle, and
shipping networking.service disabled so MAAS owns the network on first boot.
Specific to this image, the Debian cloud kernel is replaced with the generic one
— a bare-metal node booted with the cloud kernel can come up with no disk.
Nothing here has been built or deployed. The README says so at the top and in a
Verified status section that separates what was checked from what was not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>