425 lines
18 KiB
Markdown
425 lines
18 KiB
Markdown
# Samba Active Directory Domain Controller
|
||
|
||
Samba AD DC is the **identity source of truth** for the whole environment: Kerberos,
|
||
LDAP, DNS, SMB and Group Policy — a real Active Directory domain that Windows VMs can
|
||
domain-join and be GPO-managed, **without a licensed Windows Server as the DC**.
|
||
|
||
Consumers layered on top (later docs):
|
||
- **Samba file server** — domain member serving SMB shares.
|
||
- **Linux clients** — `realmd`/`sssd` domain join for central login.
|
||
- **Authelia** — LDAP source against this DC → OIDC / forward-auth web SSO.
|
||
|
||
This document covers **Phase 1 (the DC)** and **Phase 2 (the Windows admin box)** only.
|
||
|
||
**Everything here is managed as code with Ansible** — see `ansible/`. The shell
|
||
commands in §1–§2 are documentation of *what the roles automate*, not steps to run by
|
||
hand. Run the playbooks.
|
||
|
||
---
|
||
|
||
## Ansible quickstart
|
||
|
||
```bash
|
||
cd ansible
|
||
|
||
# 1. one-time: ansible + collections + WinRM lib
|
||
# (uv-installed ansible: uv tool install ansible-core --with ansible --with pywinrm --with requests-ntlm)
|
||
ansible-galaxy collection install -r requirements.yml
|
||
|
||
# 2. set your values (already filled in for ad.ddupan.top / DDUPAN / 192.168.10.5)
|
||
$EDITOR group_vars/all/vars.yml # realm, netbios, DC IP, forwarder, KMS host
|
||
$EDITOR inventory/hosts.yml # DC + Windows box addresses
|
||
cp group_vars/all/vault.example.yml group_vars/all/vault.yml
|
||
$EDITOR group_vars/all/vault.yml # admin passwords
|
||
ansible-vault encrypt group_vars/all/vault.yml # optional; plaintext also works (gitignored)
|
||
|
||
# 3. create the VM locally (libvirt + Ubuntu cloud image + cloud-init)
|
||
ssh-keygen -t ed25519 # if you don't have ~/.ssh/id_ed25519.pub yet
|
||
ansible-playbook create-dc-vm.yml # boots dc1 on br0 at 192.168.10.5
|
||
|
||
# 4. provision the domain, then verify
|
||
ansible-playbook provision-dc.yml --ask-vault-pass
|
||
ansible-playbook provision-dc.yml --ask-vault-pass --tags verify
|
||
|
||
# 5. join + configure the Windows admin box (needs WinRM reachable)
|
||
ansible-playbook join-windows.yml --ask-vault-pass
|
||
```
|
||
|
||
Re-running is safe: domain provisioning is guarded by the `sam.ldb` database, so a
|
||
second run is a no-op there and only reconciles config drift.
|
||
|
||
**Layout**
|
||
|
||
| Path | Purpose |
|
||
|---|---|
|
||
| `roles/dc_vm/` | creates the DC VM locally via libvirt + cloud-init |
|
||
| `roles/samba_ad_dc/` | provisions the DC (§1) — idempotent |
|
||
| `ansible/collections/ansible_collections/ddupan/homelab/` | repository-local collection containing the Samba DNS module and its `ansible-test` tests |
|
||
| `roles/samba_ad_dc/tasks/verify.yml` | smoke tests (`--tags verify`) |
|
||
| `roles/samba_ad_dc/tasks/legacy.yml` | opt-in retro-client protocols (§ Retro clients) |
|
||
| `roles/windows_vm/` | unattended-installs the Windows Server 2025 admin box (§2.0) |
|
||
| `roles/win_domain_join/` | joins Windows box, installs RSAT, KMS activation (§2) |
|
||
| `group_vars/all/vars.yml` | all non-secret settings — **edit this** |
|
||
| `group_vars/all/vault.yml` | admin passwords (gitignored; encrypt with ansible-vault) |
|
||
| `inventory/hosts.yml` | DC + Windows hosts |
|
||
|
||
Static DNS records are declared once in `../../dns/records.yml`. The role manages
|
||
only those RRsets and deliberately leaves Samba-generated AD locator records alone.
|
||
|
||
---
|
||
|
||
## 0. Decisions — fill these in before touching anything
|
||
|
||
These map 1:1 onto `ansible/group_vars/all.yml`.
|
||
|
||
|
||
| Item | Value | Notes |
|
||
|---|---|---|
|
||
| DNS realm | `AD.DDUPAN.TOP` | Delegated subdomain of `ddupan.top`. Never `.local`. |
|
||
| NetBIOS / short name | `DDUPAN` | ≤15 chars, uppercase, no dots. Windows-visible short domain. |
|
||
| DC hostname (FQDN) | `dc1.ad.ddupan.top` | |
|
||
| DC static IP | `192.168.10.5/24` | On `br0`; free in the `.2–.9` range. **Must stay static.** |
|
||
| Gateway | `192.168.10.1` | |
|
||
| Domain admin | `Administrator` | password from the vault |
|
||
| Forwarder DNS | `192.168.10.1` | where the DC forwards non-AD lookups |
|
||
| Windows admin box | `192.168.10.6` | RSAT/GPMC station |
|
||
|
||
> The example shell in §1–§2 still shows `example.com`-style placeholders for
|
||
> readability; the **live values above** are what `group_vars/all.yml` actually sets.
|
||
|
||
---
|
||
|
||
## 1. Provision the DC VM
|
||
|
||
> Automated by `roles/samba_ad_dc`. The commands below are what each task does.
|
||
|
||
**Base:** Debian 12 (or Ubuntu 24.04 LTS) VM — a **full VM, not LXC**. The DC is a
|
||
stateful pet; a VM avoids LXC keyring/namespace quirks with Kerberos. (The VM must
|
||
already exist and be reachable over SSH; this role configures the OS, it does not
|
||
create the VM — see "VM lifecycle" below.)
|
||
|
||
### 1.1 Host prerequisites
|
||
|
||
```bash
|
||
# Static IP, correct hostname, and time sync are non-negotiable.
|
||
hostnamectl set-hostname dc1.ad.example.com
|
||
timedatectl set-ntp true # or install chrony; Kerberos dies on >5min skew
|
||
apt update && apt install -y chrony
|
||
```
|
||
|
||
`/etc/hosts` — the DC must resolve its own FQDN to its **real** IP (not 127.0.1.1):
|
||
|
||
```
|
||
10.10.10.10 dc1.ad.example.com dc1
|
||
```
|
||
|
||
### 1.2 Free up port 53
|
||
|
||
Samba's internal DNS must bind `:53`. Disable the `systemd-resolved` stub listener:
|
||
|
||
```bash
|
||
mkdir -p /etc/systemd/resolved.conf.d
|
||
printf '[Resolve]\nDNSStubListener=no\n' > /etc/systemd/resolved.conf.d/no-stub.conf
|
||
systemctl restart systemd-resolved
|
||
# Point the DC at itself for DNS:
|
||
ln -sf /run/systemd/resolve/resolv.conf /dev/null 2>/dev/null || true
|
||
printf 'nameserver 10.10.10.10\nsearch ad.example.com\n' > /etc/resolv.conf
|
||
chattr +i /etc/resolv.conf # stop NM/cloud-init from clobbering it
|
||
```
|
||
|
||
### 1.3 Install and provision
|
||
|
||
```bash
|
||
apt install -y samba krb5-config winbind smbclient ldb-tools
|
||
|
||
# Stop distro auto-started daemons; the AD DC runs the unified `samba` service only.
|
||
systemctl disable --now smbd nmbd winbind 2>/dev/null || true
|
||
systemctl unmask samba-ad-dc
|
||
|
||
# Clean any stock config so provision writes fresh
|
||
mv /etc/samba/smb.conf /etc/samba/smb.conf.orig 2>/dev/null || true
|
||
|
||
samba-tool domain provision \
|
||
--use-rfc2307 \
|
||
--realm=AD.EXAMPLE.COM \
|
||
--domain=EXAMPLE \
|
||
--server-role=dc \
|
||
--dns-backend=SAMBA_INTERNAL \
|
||
--adminpass='CHANGE-ME-Strong.Passw0rd' \
|
||
--option="dns forwarder = 10.10.10.1"
|
||
|
||
systemctl enable --now samba-ad-dc
|
||
```
|
||
|
||
> **`--use-rfc2307` is mandatory and cannot be cleanly added later.** It stores POSIX
|
||
> uid/gid in the directory so Linux clients get stable IDs.
|
||
|
||
### 1.4 Wire Kerberos
|
||
|
||
```bash
|
||
cp /var/lib/samba/private/krb5.conf /etc/krb5.conf
|
||
```
|
||
|
||
### 1.5 Smoke tests
|
||
|
||
```bash
|
||
# LDAP / domain sanity
|
||
samba-tool domain level show
|
||
|
||
# Kerberos ticket for the admin
|
||
kinit [email protected] && klist
|
||
|
||
# DNS: the DC must resolve its own SRV records
|
||
host -t SRV _ldap._tcp.ad.example.com.
|
||
host -t SRV _kerberos._udp.ad.example.com.
|
||
host -t A dc1.ad.example.com.
|
||
|
||
# SMB: default shares present
|
||
smbclient -L localhost -U administrator
|
||
```
|
||
|
||
All four must succeed before moving on. **If anything AD-related "doesn't work"
|
||
later, it is almost always DNS** — every member must use the DC as its resolver, and
|
||
the DC must resolve itself.
|
||
|
||
---
|
||
|
||
## 2. Windows admin box (your one long-lived Windows VM)
|
||
|
||
You need exactly **one** Windows VM as the GPO authoring / AD management station.
|
||
Samba stores GPOs but you edit them with the Windows GPMC; `samba-tool gpo` only
|
||
covers basic operations.
|
||
|
||
### 2.0 Build it as code — `create-windows-vm.yml` (role `windows_vm`)
|
||
|
||
Fully unattended install of **Windows Server 2025 Standard (Desktop Experience)** from
|
||
your ISO, the Windows analogue of the DC's cloud-init:
|
||
|
||
```bash
|
||
ansible-playbook create-windows-vm.yml # ~30-45 min: install + reboots + WinRM setup
|
||
```
|
||
|
||
What it does:
|
||
- Creates zvol `data/vm/winadmin`, **UEFI/GPT**, **virtio disk + virtio NIC**.
|
||
- Renders `autounattend.xml` (edition index 2, locale zh-CN, GVLK, admin pw from vault),
|
||
packs it on a seed ISO. **Injects virtio drivers** (`viostor`/`NetKVM`, 2k25) during
|
||
Setup via `DriverPaths` off the `virtio-win` ISO so Setup sees the virtio disk.
|
||
- `FirstLogonCommands`: static IP `192.168.10.6` → DC DNS, **WinRM-over-HTTPS** listener
|
||
(self-signed), open 5986, **enable RDP**, and run **virtio-win guest tools** (QEMU
|
||
guest agent + balloon).
|
||
- Gets past "Press any key to boot from CD" with `virsh send-key`, waits for WinRM,
|
||
then ejects install media and deletes the password-bearing seed ISO.
|
||
|
||
Then hand off to §2.1–2.4 below, automated by `join-windows.yml` (role `win_domain_join`)
|
||
— domain join → RSAT → KMS activate. Needs WinRM reachable; the commands below are what
|
||
those tasks do.
|
||
|
||
> The seed ISO holds the local admin password in cleartext while Setup runs; the play
|
||
> removes it once WinRM is up. Edition index / GVLK / locale are role vars — verify the
|
||
> index against your ISO with `wiminfo …\sources\install.wim`.
|
||
|
||
### 2.1 Point it at the DC for DNS
|
||
|
||
On the Windows VM's NIC, set **DNS server = `10.10.10.10`** (the DC). This is the
|
||
single most common failure point — a Windows box using any other resolver cannot
|
||
find the domain.
|
||
|
||
### 2.2 Join the domain
|
||
|
||
`System → Rename this PC (advanced) → Domain: ad.example.com` →
|
||
authenticate as `EXAMPLE\Administrator` → reboot.
|
||
|
||
### 2.3 Install RSAT (management tools)
|
||
|
||
On Windows 10/11:
|
||
|
||
```powershell
|
||
Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
|
||
Add-WindowsCapability -Online -Name Rsat.GroupPolicy.Management.Tools~~~~0.0.1.0
|
||
Add-WindowsCapability -Online -Name Rsat.Dns.Tools~~~~0.0.1.0
|
||
```
|
||
|
||
This gives you **AD Users & Computers**, **Group Policy Management (GPMC)**, and the
|
||
DNS console — the graphical admin surface for the domain.
|
||
|
||
### 2.4 Activate against existing KMS
|
||
|
||
This repo already runs `vlmcsd` (KMS emulator). Activate the Windows VM against it so
|
||
the admin box stays licensed:
|
||
|
||
```powershell
|
||
slmgr /skms kms.example.com:1688 # your vlmcsd host
|
||
slmgr /ipk <GVLK-for-this-edition> # public KMS client setup key for the edition
|
||
slmgr /ato
|
||
```
|
||
|
||
---
|
||
|
||
## VM lifecycle (creating the box as code)
|
||
|
||
The VM itself is created by `roles/dc_vm` (`create-dc-vm.yml`), which runs against
|
||
the **local libvirt host**:
|
||
|
||
1. Downloads the **latest Ubuntu 24.04 LTS cloud image** (`.../noble/current/…`) once
|
||
into `/var/lib/libvirt/images/base/`.
|
||
2. Creates a **ZFS zvol** `data/vm/dc1` (`dc_vm_disk_gb`, default 25 GB, 16K
|
||
volblocksize — matching `data/vm/win2k25`) and writes the image into it raw with
|
||
`qemu-img convert`. cloud-init `growpart` expands the rootfs to fill it on first boot.
|
||
3. Renders `user-data` / `meta-data` / `network-config` from templates and packs a
|
||
**NoCloud seed ISO** (`genisoimage`, volid `cidata`). The seed sets a static IP
|
||
(`192.168.10.5/24`, gw `.1`), the hostname, and injects your SSH public key.
|
||
4. `virt-install --import` attaches the zvol (`/dev/zvol/data/vm/dc1`, virtio) and the
|
||
seed ISO, boots the domain on bridge **`br0`**, then waits for SSH.
|
||
|
||
It's idempotent — if the `dc1` domain exists the block is skipped; the zvol write is
|
||
separately guarded so a re-run never clobbers an existing disk.
|
||
|
||
**Before running:** you need an SSH keypair; the play injects
|
||
`dc_vm_ssh_pubkey_file` (default `~/.ssh/id_ed25519.pub`). Generate one with
|
||
`ssh-keygen -t ed25519` if absent — the play fails fast otherwise.
|
||
|
||
Tunables live in `roles/dc_vm/defaults/main.yml` (vCPU, RAM, disk, image URL,
|
||
bridge, gateway, `dc_vm_zvol_parent`). Alternatives (PXE via your existing `netboot/`, or a Debian base)
|
||
are still viable, but the contract is the same: hand `samba_ad_dc` a booted VM with a
|
||
static IP matching `samba_ad_dc_ip`, SSH reachable as the `ansible` user.
|
||
|
||
---
|
||
|
||
## 3. Operations
|
||
|
||
### Add users / groups (either UI or CLI)
|
||
|
||
```bash
|
||
samba-tool user create alice
|
||
samba-tool group add engineering
|
||
samba-tool group addmembers engineering alice
|
||
```
|
||
|
||
…or do it graphically from the Windows admin box via **AD Users & Computers**.
|
||
|
||
### Backups — do this before the domain becomes load-bearing
|
||
|
||
```bash
|
||
samba-tool domain backup offline --targetdir=/var/backups/samba
|
||
```
|
||
|
||
Schedule it (cron/systemd timer) and ship the tarball off-box.
|
||
|
||
### Second DC (resilience) — easy now, painful to retrofit after an outage
|
||
|
||
On a second freshly-provisioned member (DNS pointed at `dc1`):
|
||
|
||
```bash
|
||
samba-tool domain join ad.example.com DC \
|
||
-U"EXAMPLE\administrator" --dns-backend=SAMBA_INTERNAL
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Gotchas cheat-sheet
|
||
|
||
- **DNS is the whole ballgame.** ~90% of "AD is broken" is a member not using the DC
|
||
as its resolver, or the DC not resolving itself. Domain members point at
|
||
`192.168.10.5` directly; non-domain LAN devices reach AD names via the router's
|
||
conditional forward — see `router-dns-nec-ix.md`.
|
||
- **Time skew > 5 min → Kerberos silently fails.** Keep `chrony` healthy on the DC and
|
||
all members.
|
||
- **Don't containerize the DC in K8s** — it fights K8s's DNS/networking model. VM only.
|
||
- **`--use-rfc2307` at provision time** or you lose stable Linux uid/gid mapping.
|
||
- **One share off the DC is fine for testing, but real shares belong on a member
|
||
file server** (next doc), not the DC.
|
||
- **GPO editing needs the Windows RSAT box**; keep that one VM around.
|
||
|
||
---
|
||
|
||
## Retro clients (Win9x / NT4 / Windows 2000 / XP)
|
||
|
||
Modern Samba disables the old protocols these machines need (SMB1, NTLMv1, LANMAN,
|
||
DES Kerberos). Support is **opt-in** via `samba_ad_legacy_clients: true` in
|
||
`group_vars/all.yml`, which runs `roles/samba_ad_dc/tasks/legacy.yml` to inject the
|
||
insecure knobs and restart the DC.
|
||
|
||
> ⚠️ This materially weakens the **entire domain** — LM/NTLMv1 hashes are trivially
|
||
> crackable and SMB1 is wormable. Only enable it on an **isolated VLAN** for the retro
|
||
> machines. Ideally, don't weaken the primary DC at all: stand up a **dedicated legacy
|
||
> Samba member server** for file access and keep the DC strict. For domain *logon* of
|
||
> 9x/NT4 (below) the DC itself must speak old crypto, so segment the network instead.
|
||
|
||
What each era can actually do:
|
||
|
||
| Client | Domain **join** | Domain **logon** | SMB **share access** | What it needs |
|
||
|---|---|---|---|---|
|
||
| **Windows XP / 2003** (NT 5.1/5.2) | ✅ real AD join | ✅ Kerberos/NTLMv2 | ✅ | SMB1 (`server min protocol = NT1`). Mostly works with the toggle. |
|
||
| **Windows 2000** (NT 5.0) | ✅ real AD join | ✅ | ✅ | SMB1 **+** weak Kerberos crypto (DES) — `allow_weak_crypto`. Fiddly. |
|
||
| **Windows NT4** | ❌ (no Kerberos) | ✅ NT4-style (NTLM) | ✅ | NTLM + `allow nt4 crypto`. Joins as an NT4 domain, not AD. |
|
||
| **Windows 9x / ME** | ❌ (not a domain member) | ⚠️ NT-domain network logon only | ✅ | LANMAN auth + WINS. No Kerberos, ever. LM hashes only. |
|
||
|
||
The toggle turns on (see `legacy.yml`):
|
||
|
||
```
|
||
server min protocol = NT1 # SMB1 for XP/2000/NT4/9x
|
||
ntlm auth = ntlmv1-permitted # NTLMv1
|
||
lanman auth = yes # LANMAN — required by 9x, insecure
|
||
client lanman auth = yes
|
||
allow nt4 crypto = yes # NT4 member logon
|
||
wins support = yes # NetBIOS name resolution for 9x/NT4
|
||
```
|
||
plus `allow_weak_crypto = true` in `/etc/krb5.conf` for Windows 2000's DES Kerberos.
|
||
|
||
Practical notes for the oldest clients:
|
||
- **Win9x needs WINS**, not DNS — point the clients' WINS server at the DC's IP (or a
|
||
legacy member running `wins support = yes`). Browsing relies on NetBIOS.
|
||
- **9x can't join** — it does an NT-domain *network logon* to a logon server and then
|
||
accesses `\\server\share` with domain credentials. Set the client's "Logon to
|
||
Windows NT domain" to `DDUPAN`.
|
||
- **Passwords:** after enabling weak crypto, accounts used by Win2000 may need a
|
||
password reset so the DES/RC4 keys get regenerated with the new enctypes allowed.
|
||
- Keep these clients on `192.168.10.x` with the DC as WINS + gateway, firewalled off
|
||
from anything you care about.
|
||
|
||
---
|
||
|
||
## 5. Linux Samba member fileserver (`join-member.yml`)
|
||
|
||
Joins a Linux host to the domain as a **Samba member fileserver** (`security = ADS`
|
||
+ winbind), so AD users/groups can authenticate to its SMB shares. Codified as the
|
||
`samba_member` role; runs against the `samba_members` inventory group.
|
||
|
||
```bash
|
||
# Inventory already has `laptop` (the KVM host, 192.168.10.127) under samba_members.
|
||
ansible-playbook join-member.yml --check --diff # preview (safe; no real join)
|
||
ansible-playbook join-member.yml # join for real → creates LAPTOP$ in AD
|
||
ansible-playbook join-member.yml --tags verify # re-run smoke tests only
|
||
```
|
||
|
||
What the role does, in order: install `winbind` + `libnss-winbind` + `krb5-user`
|
||
(**no `libpam-winbind`** — this is a fileserver join, not OS login) → write
|
||
`/etc/krb5.conf` for the realm → add a split-DNS drop-in → rewrite `smb.conf`
|
||
(`[global]` becomes an ADS member; existing shares preserved, original saved to
|
||
`smb.conf.pre-ads`) → add `winbind` to NSS `passwd`/`group` → `net ads join`
|
||
(guarded by `net ads testjoin`, so re-runs are no-ops) → start winbind → smoke tests.
|
||
|
||
Design choices:
|
||
- **RID idmap** (`idmap config DDUPAN : backend = rid`, range `10000-999999`) —
|
||
algorithmic, deterministic, needs no RFC2307 attributes. The DC *is* `--use-rfc2307`,
|
||
so switching to `backend = ad` for centrally-managed `uidNumber`/`gidNumber` is a
|
||
later option (must be identical on every member). Administrator → uid `10500`.
|
||
- **Split-DNS is mandatory here.** The LAN router (`192.168.10.1`) does **not** answer
|
||
realm SRV lookups, so the role drops `/etc/systemd/resolved.conf.d/ad-realm.conf`
|
||
routing `ad.ddupan.top` → the DC (`192.168.10.5`), which is authoritative and fast.
|
||
- **`apt update` is OFF by default** (`samba_member_apt_update_cache: false`). That
|
||
host's upstream DNS forwarder is flaky and a full refresh touches every repo in
|
||
`sources.list.d`; the member packages are already in the local cache. Pass
|
||
`-e samba_member_apt_update_cache=true` to force a refresh when DNS is healthy.
|
||
- Shares are data (`samba_member_shares` in the role defaults) — the guest `[win]`
|
||
share and the Cockpit ZFS `include` are carried over verbatim.
|
||
|
||
Verify by hand: `sudo net ads testjoin` → *Join is OK*; `wbinfo --online-status`;
|
||
`getent group 'domain admins'`; `smbclient -L localhost -N`.
|
||
|
||
## Next docs
|
||
|
||
- ~~`samba-fileserver` — domain-member SMB shares.~~ **Done** — see §5 above.
|
||
- `linux-domain-join` — `realmd` + `sssd` (OS login, distinct from this fileserver join).
|
||
- `../../apps/authelia/` — LDAP source → OIDC / forward-auth web SSO.
|