# Installation on Debian 13

> This expects a freshly installed Debian 13 (trixie) VM. **Not** an LXC container.
> Debian 13 ships Samba 4.22, which supports AD functional level 2016.

## What you need

Decide on hostname, domain and IP before you start. Changing any of them after
provisioning means starting over.

This guide uses:

| Item | Value |
| --- | --- |
| Hostname | `srv-dc01` |
| DNS domain (realm) | `samba-test.lan` |
| NetBIOS domain | `SAMBA-TEST` |
| IP / prefix | `10.0.0.5/20` |
| Gateway | `10.0.0.254` |
| Upstream DNS | `10.0.0.254` |

> NetBIOS domain names are limited to 15 characters. Hyphens are allowed.

---

## 1. Preparation

### Updates

```console
apt update
apt full-upgrade -y
```

### Core dependencies

```console
apt install -y chrony vim ethtool
```

### Hostname

```console
hostnamectl set-hostname srv-dc01
```

Configure `/etc/hosts`. Remove the Debian default `127.0.1.1` line — the FQDN
must resolve to the real IP:

```conf
127.0.0.1       localhost
10.0.0.5        srv-dc01.samba-test.lan srv-dc01
```

Verify:

```console
hostname -f
```

Must return `srv-dc01.samba-test.lan`. If it does not, provisioning will fail.

### Network

Modify `/etc/network/interfaces`.

> Interface `ens18` might be named differently for you.

```conf
auto ens18
iface ens18 inet static
  address 10.0.0.5/20
  gateway 10.0.0.254
```

### Disable the DHCP client

**Do not skip this.** It is the single most common way to break a Samba DC on
Debian 13, and the failure only shows up much later as clients that cannot
join the domain.

Since Trixie, `dhcpcd-base` has priority `important` instead of
`isc-dhcp-client` on new installations. The Debian installer configures the
interface via DHCP, and `ifup@<interface>.service` starts `dhcpcd` at boot.
Switching `/etc/network/interfaces` to a static address afterwards does **not**
stop the already running daemon — it keeps its lease, adds a second address to
the interface and manages the default route.

Samba then registers *both* addresses in DNS. The Windows DC locator tries
them in turn, hits the dead one and gives up.

Check:

```console
ps aux | grep '[d]hcpcd'
ip -4 addr show ens18
```

If a `dhcpcd` process exists or the interface has a `secondary dynamic`
address, find out what holds it:

```console
systemctl status <pid-of-dhcpcd>
```

Note that `dhcpcd -x` does **not** work here — `ifupdown` starts the daemon
per interface with its own control socket, not in master mode.

The clean fix is a reboot with the static configuration already in place:

```console
echo 'denyinterfaces ens18' >> /etc/dhcpcd.conf
reboot
```

> If you are connected over SSH, do not try to stop the interface manually —
> you will lock yourself out. Either reboot, or work from the VM console.

After the reboot, verify:

```console
ip -4 addr show ens18        # only 10.0.0.5/20
ip route                     # one default route via 10.0.0.254
ps aux | grep '[d]hcpcd'     # empty
```

Also reserve or exclude the DC's address in your DHCP scope, so the pool never
hands `10.0.0.5` to another machine.

### Time synchronisation

```console
systemctl enable --now chrony
chronyc tracking
```

Kerberos tolerates a clock skew of ±5 minutes. Aim for an offset well under
5 seconds — anything larger means chrony has not converged yet.

Signed NTP for Windows clients is configured later (section 7), because the
socket directory does not exist until after provisioning.

### Disable `systemd-resolved`

Samba's internal DNS server needs port 53.

```console
systemctl disable --now systemd-resolved
rm -f /etc/resolv.conf
```

Create `/etc/resolv.conf`:

> For provisioning, point `nameserver` at your router or firewall so `apt`
> keeps working. It is switched to the DC itself in section 5.

```conf
nameserver 10.0.0.254
search samba-test.lan
```

> `/etc/resolv.conf` must never be left absent or empty. `samba_dnsupdate`
> reads it directly and aborts with
> `dns.resolver.NoResolverConfiguration: no nameservers` — which silently
> skips DNS record maintenance.

Ensure nothing is listening on port 53:

```console
ss -tlnp | grep :53
```

This must return nothing.

---

## 2. Install Samba

```console
apt install -y acl attr samba winbind libpam-winbind libnss-winbind \
  krb5-config krb5-user dnsutils python3-setproctitle samba-ad-dc
```

`krb5-config` prompts during installation:

- **Default Kerberos version 5 realm**: `SAMBA-TEST.LAN` (uppercase)
- **Kerberos servers for your realm**: `srv-dc01.samba-test.lan`
- **Administrative server for your realm**: `srv-dc01.samba-test.lan`

These values are transient — `/etc/krb5.conf` is replaced in section 5.

Verify the AD DC modules are present:

```console
dpkg -l samba-dsdb-modules samba-vfs-modules
samba -V
```

Without `samba-dsdb-modules` the DC will not start.

### Mask conflicting services

Debian starts `smbd`, `nmbd` and `winbind` as standalone services immediately
after installation. On an AD DC these must not run — the `samba` process
provides its own internal file server and winbind. Leaving them active causes
`samba-dcerpcd` to crash-loop and NSS lookups to hang.

```console
systemctl disable --now smbd nmbd winbind
systemctl mask smbd nmbd winbind
```

---

## 3. Domain provisioning

Remove the packaged configuration file:

```console
rm -f /etc/samba/smb.conf
```

Choose **one** of the two variants below.

> **Building a replacement for an existing domain?** Then you do not provision
> at all — you join the existing domain as an additional DC. Skip to
> section 12 and come back here only for a standalone lab domain.

### Variant A — default functional level (2008 R2)

Conservative, fully supported, matches most existing Samba deployments.

```console
samba-tool domain provision --use-rfc2307 --realm=SAMBA-TEST.LAN \
  --domain=SAMBA-TEST --server-role=dc --dns-backend=SAMBA_INTERNAL \
  --adminpass='ChangeMe123!'
```

### Variant B — functional level 2016

Required if the domain this DC will eventually replace runs at FL 2012 R2 or
2016, and needed for Kerberos claims, authentication policies and
authentication silos.

```console
samba-tool domain provision --use-rfc2307 --realm=SAMBA-TEST.LAN \
  --domain=SAMBA-TEST --server-role=dc --dns-backend=SAMBA_INTERNAL \
  --adminpass='ChangeMe123!' \
  --function-level=2016 \
  --option="ad dc functional level = 2016"
```

The `--option` only applies during provisioning. It **must** also be written
permanently into `smb.conf` (section 5) — on every DC in the domain.

> **Caveats for FL 2016.** Samba's implementation is partial and not enabled by
> default. Samba reads and writes claims and populates them into the PAC, but
> does not yet use them for access control decisions. Microsoft's PowerShell
> based AD tools are not expected to work. Functional levels cannot be
> lowered again.

> If you omit `--adminpass`, a random password is generated and printed to the
> console in cleartext, where it lands in your scrollback and terminal logs.

---

## 4. Deploy Kerberos configuration

```console
cp /var/lib/samba/private/krb5.conf /etc/krb5.conf
```

Copy the file — do not symlink it.

---

## 5. Configure DNS and `smb.conf`

Point the resolver at the DC itself. Rewrite `/etc/resolv.conf`:

```conf
nameserver 10.0.0.5
search samba-test.lan
```

Add the following to the **existing** `[global]` section of
`/etc/samba/smb.conf` — do not create a second `[global]` block:

```conf
        dns forwarder = 10.0.0.254

        # Variant B only — must be present on every DC
        ad dc functional level = 2016
```

On first startup, `ad dc functional level` updates the server's own AD entry
with the configured level.

Check the file parses:

```console
testparm -s
```

---

## 6. Start Samba

```console
systemctl unmask samba-ad-dc
systemctl enable --now samba-ad-dc
systemctl status samba-ad-dc
```

If the service fails to start, the two usual causes are a port 53 conflict
(check `ss -tlnp | grep :53`) and `server role` not being set to
`active directory domain controller` in `smb.conf`.

---

## 7. Post-provisioning tasks

### Set the domain administrator password

```console
samba-tool user setpassword Administrator
```

### Review the password policy

```console
samba-tool domain passwordsettings show
```

Samba defaults to a maximum password age of 43 days. In a lab this will expire
on you at the worst possible moment:

```console
samba-tool domain passwordsettings set --max-pwd-age=0
```

Do not do this in production — use the policy your organisation requires.

### Verify sysvol ACLs

```console
samba-tool ntacl sysvolcheck
```

If it reports errors:

```console
samba-tool ntacl sysvolreset
```

### Create the reverse DNS zone

Provisioning does not create one.

```console
samba-tool dns zonecreate srv-dc01 0.0.10.in-addr.arpa -U Administrator
```

> A `/20` network spans `10.0.0.x` through `10.0.15.x`. Either create all 16
> zones or use `10.in-addr.arpa` instead.

### Signed NTP for Windows clients

Domain members expect authenticated time (MS-SNTP) from a DC. Without this,
clients stay on `Local CMOS Clock`, drift out of the ±5 minute Kerberos window
and eventually fail authentication with `KRB_AP_ERR_SKEW`.

The socket directory exists only after provisioning. Samba creates it as
`root:root`, but chrony runs as `_chrony` and needs group access:

```console
chgrp _chrony /var/lib/samba/ntp_signd
chmod 750 /var/lib/samba/ntp_signd
ls -ld /var/lib/samba/ntp_signd
```

Make the ownership survive — a `samba-ad-dc` restart can recreate the
directory and reset the group, after which chrony silently drops back to
unsigned operation:

```console
echo 'd /var/lib/samba/ntp_signd 0750 root _chrony -' > /etc/tmpfiles.d/samba-ntp-signd.conf
systemd-tmpfiles --create
```

Add to `/etc/chrony/chrony.conf`:

```conf
ntpsigndsocket /var/lib/samba/ntp_signd
allow 10.0.0.0/20
```

```console
systemctl restart chrony
```

**Verify — do not skip.** Both checks must pass:

```console
ss -ulnp | grep :123
journalctl -u chrony -n 20 --no-pager | grep -i 'MS-SNTP'
```

Expected: a listener on `0.0.0.0:123`, and the log line
`MS-SNTP authentication enabled`. If that line is missing, chrony started
without the signing socket — almost always a permissions problem on
`/var/lib/samba/ntp_signd`. Fix it and restart chrony; there is no warning
about this in normal operation.

Also confirm chrony itself is synchronised upstream:

```console
chronyc tracking
chronyc sources
```

A DC that is not synchronised cannot serve time to anyone.

### Take a snapshot

Snapshot the VM here — freshly provisioned, no clients joined. Domain join
tests can then be repeated from a known state.

---

## 8. Verification

### Services and listeners

```console
ss -tlnp | grep -E ':(53|88|389|445|464|636)\b'
smbclient -L localhost -N
```

Expected listeners: 53 (DNS), 88 (Kerberos), 389 (LDAP), 445 (SMB),
464 (kpasswd), 636 (LDAPS). `smbclient` must show the `netlogon` and `sysvol`
shares.

### DNS records

```console
host -t SRV _ldap._tcp.samba-test.lan localhost
host -t SRV _kerberos._udp.samba-test.lan localhost
host -t A srv-dc01.samba-test.lan localhost
```

Repeat from **another host** on the network — this is what clients actually do:

```console
host -t SRV _ldap._tcp.samba-test.lan 10.0.0.5
```

### Kerberos

```console
kinit administrator@SAMBA-TEST.LAN
klist
```

The ticket must show realm `SAMBA-TEST.LAN`.

### Directory consistency

```console
samba-tool dbcheck --cross-ncs
getent passwd administrator
```

`getent` proves NSS resolution through the internal winbind works.

### Functional level

```console
samba-tool domain level show
```

Variant A reports `(Windows) 2008 R2`, Variant B reports `(Windows) 2016`.

---

## 9. Raising an existing domain from 2008 R2 to 2016

Only needed if you provisioned with Variant A and now need a higher level.

> **Functional levels cannot be lowered.** Take a backup and verify it before
> starting.

### Back up first

```console
samba-tool domain backup offline --targetdir=/srv/backup
samba-tool dbcheck --cross-ncs
```

### Enable the feature

Add to the `[global]` section of `/etc/samba/smb.conf` **on every DC**:

```conf
        ad dc functional level = 2016
```

Restart Samba on every DC:

```console
systemctl restart samba-ad-dc
```

### Upgrade schema and raise the level

```console
samba-tool domain schemaupgrade --schema=2019
samba-tool domain functionalprep --function-level=2016
samba-tool domain level raise --domain-level=2016 --forest-level=2016
```

### Verify

```console
samba-tool domain level show
samba-tool dbcheck --cross-ncs
```

---

## 10. Domain join test

### Prepare the client

Set the DC as the **only** DNS server — not the router, not a public resolver,
not both. This is the cause of most "domain could not be contacted" errors: if
a second resolver answers first, Windows caches its `NXDOMAIN` for the domain
and never asks the DC.

```powershell
Get-DnsClientServerAddress -AddressFamily IPv4
Set-DnsClientServerAddress -InterfaceAlias "Ethernet" -ServerAddresses 10.0.0.5
Clear-DnsClientCache
```

If the addresses come from DHCP, fix option 6 in the scope — otherwise the
router returns on the next lease renewal.

Confirm the client can find a DC before attempting the join:

```powershell
Resolve-DnsName -Name samba-test.lan -Server 10.0.0.5
Resolve-DnsName -Name _ldap._tcp.dc._msdcs.samba-test.lan -Type SRV -Server 10.0.0.5
Test-NetConnection -ComputerName 10.0.0.5 -Port 389
```

The A record for the domain must return exactly **one** address. More than one
means stale records — see the appendix.

### Join and verify

After joining, from the DC:

```console
samba-tool computer list
samba-tool user list
```

The client should have registered itself in DNS via GSS-TSIG:

```console
host -t A <client-hostname>.samba-test.lan 127.0.0.1
```

If nothing comes back, dynamic updates are not working — this does not block
the join, but breaks name resolution for services later.

### Time synchronisation

```powershell
w32tm /query /configuration | findstr /i "Type"
w32tm /resync /rediscover
w32tm /query /source
w32tm /query /status
```

`Type` must be `NT5DS`. `Source` must change to `srv-dc01.samba-test.lan`,
`Stratum` must be non-zero, and `Last Successful Sync Time` must show a
timestamp. If the source stays `Local CMOS Clock`:

```powershell
Restart-Service w32time
w32tm /resync /rediscover
w32tm /stripchart /computer:srv-dc01.samba-test.lan /samples:3 /dataonly
```

On the DC, confirm the request actually arrives:

```console
chronyc clients
```

If the client appears here with NTP counters but still refuses to sync, the
replies are unsigned — go back to the MS-SNTP check in section 7.

### Domain logon

A machine join does not prove that user authentication works.

```console
samba-tool user create testuser
samba-tool group addmembers "Domain Admins" testuser
```

Log on to the client as `SAMBA-TEST\testuser`, then:

```powershell
klist
```

There must be a TGT and service tickets. Also open `\\samba-test.lan\sysvol`
in Explorer, and check group policy handling:

```console
samba-tool gpo listall
```

---

## 11. Before going to production

- **Backups.** `samba-tool domain backup offline` on a schedule, plus a tested
  restore. A DC without a verified restore path is not a DC you can rely on.
- **Second DC.** Join a second controller and verify replication with
  `samba-tool drs showrepl`.
- **sysvol replication.** Samba does **not** replicate sysvol between DCs.
  This has to be built separately with rsync or osync. This surprises people
  migrating from Windows AD with DFS-R.
- **Monitoring.** Add LDAP, Kerberos and DNS service checks plus
  `samba-tool drs showrepl` to your monitoring.
- **FSMO roles.** When replacing an existing DC, transfer the roles with
  `samba-tool fsmo transfer` before demoting the old one.

---

## 12. Alternative path: joining an existing domain

Use this **instead of** sections 3 to 5 when the new DC is meant to become part
of an existing domain — whether that domain is served by Windows DCs or by
Samba. Provisioning would create a *separate* domain that shares nothing with
the existing one.

Sections 1 and 2 (preparation, package installation, service masking) apply
unchanged. Sections 6 onwards apply after the join, with the differences noted
below.

### Prerequisites

Check these before starting. A failed join leaves partial objects in the
directory that have to be cleaned up manually.

**Functional level and schema.** Samba must support the level the existing
domain runs at. Query it from an existing DC:

```console
samba-tool domain level show -H ldap://<existing-dc> -U Administrator
```

Samba 4.22 handles up to 2016. If the source domain is higher, the join will
not work.

**Time.** The new DC must be within the Kerberos skew of the existing domain.
Point chrony at the existing DC rather than a public pool:

```conf
server <existing-dc-fqdn> iburst
```

```console
systemctl restart chrony
chronyc tracking
```

**DNS.** `/etc/resolv.conf` on the new machine must point at an existing DC —
not the router, not a public resolver:

```conf
nameserver <existing-dc-ip>
search samba-test.lan
```

Verify the domain is discoverable:

```console
host -t SRV _ldap._tcp.dc._msdcs.samba-test.lan
host -t SRV _kerberos._tcp.samba-test.lan
```

**Kerberos.** Create `/etc/krb5.conf` before joining — the generated one from
provisioning does not exist yet:

```conf
[libdefaults]
        default_realm = SAMBA-TEST.LAN
        dns_lookup_realm = false
        dns_lookup_kdc = true
```

Test it:

```console
kinit Administrator@SAMBA-TEST.LAN
klist
```

If this fails, the join will fail too. Fix it here.

**Clean slate.** If you previously provisioned on this machine, wipe it:

```console
systemctl stop samba-ad-dc
rm -f /etc/samba/smb.conf
rm -rf /var/lib/samba/private/* /var/cache/samba/* /var/lib/samba/sysvol/*
```

### Join

```console
samba-tool domain join samba-test.lan DC \
  -U Administrator \
  --dns-backend=SAMBA_INTERNAL \
  --option="ad dc functional level = 2016"
```

Useful additions:

- `--server=<existing-dc-fqdn>` — replicate from a specific DC instead of
  whichever one DNS returns. Worth setting when the source DCs are in
  different sites.
- `--site=<site-name>` — place the DC in a specific AD site. Defaults to
  `Default-First-Site-Name`.

Drop `--option="ad dc functional level = 2016"` if the domain runs below 2016.
Where it applies, it must be written permanently into `smb.conf` as well.

The join replicates the full directory. On a domain with many objects this
takes a while and produces little output — let it run.

### After the join

Point the resolver at the new DC itself and add the forwarder, as in section 5:

```conf
nameserver 10.0.0.5
search samba-test.lan
```

```conf
        dns forwarder = 10.0.0.254

        # only where the domain runs at 2016
        ad dc functional level = 2016
```

Then start Samba as in section 6:

```console
systemctl unmask samba-ad-dc
systemctl enable --now samba-ad-dc
```

Register the new DC's own DNS records:

```console
samba_dnsupdate --verbose --all-names
```

### Verify replication

This is the check that matters — the join can succeed while replication does
not.

```console
samba-tool drs showrepl
```

Every partition must show recent successful inbound and outbound replication
with zero consecutive failures. Force a run to be sure:

```console
samba-tool drs replicate <new-dc> <existing-dc> DC=samba-test,DC=lan
samba-tool drs replicate <existing-dc> <new-dc> DC=samba-test,DC=lan
```

Confirm the new DC is visible domain-wide:

```console
samba-tool computer list | grep -i srv-dc01
host -t SRV _ldap._tcp.dc._msdcs.samba-test.lan 127.0.0.1
```

The SRV record must now list both DCs.

### sysvol

**Samba does not replicate sysvol.** The join brings the directory database,
not the policy files. Copy them once from an existing DC and repair the ACLs:

```console
rsync -aAXv --delete root@<existing-dc>:/var/lib/samba/sysvol/ /var/lib/samba/sysvol/
samba-tool ntacl sysvolreset
samba-tool ntacl sysvolcheck
```

For ongoing operation this has to be automated — rsync on a timer or osync for
bidirectional sync. Coming from Windows AD with DFS-R, this is the single
biggest operational difference and the most common thing to forget.

### Replacing the old DC

Only once replication has been verified and has run cleanly for a while.

Transfer the FSMO roles:

```console
samba-tool fsmo show
samba-tool fsmo transfer --role=all -U Administrator
samba-tool fsmo show
```

Then demote the old DC. On a Samba DC:

```console
samba-tool domain demote -U Administrator
```

On a Windows DC, use `dcpromo` / Server Manager. Only if the old DC is already
gone and cannot be demoted properly:

```console
samba-tool domain demote --remove-other-dead-server=<old-dc-name> -U Administrator
```

Afterwards, clean up its DNS records and check the directory:

```console
samba-tool dns query localhost samba-test.lan @ A -U Administrator
samba-tool dbcheck --cross-ncs
samba-tool drs showrepl
```

Finally, repoint clients and DHCP option 6 at the new DC.

---

## Appendix: cleaning up stale A records

If the DC ever held a second IP address — a DHCP lease, a temporary interface,
a changed address — `samba_dnsupdate` will have registered it, and the record
survives after the address is gone. Symptom: domain joins fail intermittently,
`dcdiag` reports `DNS name does not exist` for
`_ldap._tcp.dc._msdcs.<domain>`.

**Fix the interface first.** As long as a DHCP client is running, records are
re-registered as fast as you delete them.

```console
ip -4 addr show ens18        # must show only the static address
```

Then inspect every name that carries an A record for the DC:

```console
samba-tool dns query localhost samba-test.lan @ A -U Administrator
samba-tool dns query localhost samba-test.lan srv-dc01 A -U Administrator
samba-tool dns query localhost samba-test.lan DomainDnsZones A -U Administrator
samba-tool dns query localhost samba-test.lan ForestDnsZones A -U Administrator
samba-tool dns query localhost _msdcs.samba-test.lan gc A -U Administrator
```

The simplest repair is to let Samba do it — with a valid `/etc/resolv.conf` in
place, `samba_dnsupdate` removes stale entries by itself:

```console
printf 'nameserver 127.0.0.1\nsearch samba-test.lan\n' > /etc/resolv.conf
samba_dnsupdate --verbose --all-names
```

Watch for `need delete:` lines in the output — those are the stale records.
The run must finish without a traceback, otherwise the deletes never happen.

To remove records manually instead, per name and per address:

```console
samba-tool dns delete localhost samba-test.lan @ A 10.0.1.108 -U Administrator
samba-tool dns delete localhost samba-test.lan srv-dc01 A 10.0.1.108 -U Administrator
samba-tool dns delete localhost samba-test.lan DomainDnsZones A 10.0.1.108 -U Administrator
samba-tool dns delete localhost samba-test.lan ForestDnsZones A 10.0.1.108 -U Administrator
samba-tool dns delete localhost _msdcs.samba-test.lan gc A 10.0.1.108 -U Administrator
```

`WERR_DNS_ERROR_RECORD_DOES_NOT_EXIST` simply means that name was already
clean.

Verify:

```console
host -t A samba-test.lan 127.0.0.1
host -t A srv-dc01.samba-test.lan 127.0.0.1
host -t A gc._msdcs.samba-test.lan 127.0.0.1
```

Each must return exactly one address. Finally, flush the client cache —
Windows will otherwise keep serving the old answers:

```powershell
Clear-DnsClientCache
Resolve-DnsName -Name samba-test.lan -Server 10.0.0.5
```