Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
@@ -0,0 +1,80 @@
---
# samba_member role defaults — Samba as an Active Directory DOMAIN MEMBER (fileserver).
# Joins the box to the domain provisioned by the samba_ad_dc role and lets AD users
# authenticate to its SMB shares via winbind. The existing standalone shares are
# preserved (see samba_member_shares below) and the machine's own login stack is
# left untouched (NSS gains winbind, but PAM is NOT modified — no AD OS login).
#
# Domain identity (samba_ad_realm / samba_ad_domain / samba_ad_dc_ip) is inherited
# from group_vars/all/vars.yml — the same values the DC was provisioned with.
# NetBIOS name of THIS member (<=15 chars, uppercase). Defaults to the short hostname.
samba_member_netbios_name: "{{ ansible_facts['hostname'] | upper }}"
# --- ID mapping ---------------------------------------------------------------
# Algorithmic RID backend: deterministic UIDs/GIDs from the AD RID, no RFC2307
# attributes required. (The DC was provisioned --use-rfc2307, so switching to the
# `ad` backend for centrally-managed uidNumber/gidNumber is possible later — see README.)
samba_member_idmap_default_range: "3000-7999" # idmap config * (local/builtin)
samba_member_idmap_domain_range: "10000-999999" # idmap config DOMAIN (rid)
# Strip the DOMAIN\ prefix so AD users appear as bare names (e.g. `alice`, not
# `DDUPAN\alice`). Only shadows a local account if an AD user shares its name.
samba_member_use_default_domain: true
# Shell/home template applied to AD users by winbind.
samba_member_template_shell: "/bin/bash"
samba_member_template_homedir: "/home/%U"
# --- Split-DNS ----------------------------------------------------------------
# The domain member MUST resolve _ldap._tcp / _kerberos._udp SRV records for the
# realm to discover the DC. The LAN router does NOT forward ad.ddupan.top here, so
# route just the realm to the DC via a systemd-resolved drop-in. Set false if your
# resolver already answers realm SRV lookups.
samba_member_configure_split_dns: true
# --- Packages -----------------------------------------------------------------
# Deliberately NO libpam-winbind: this is a fileserver join, not an OS-login join.
# libnss-winbind IS included so `getent passwd` / `ls -l` resolve AD owners.
#
# Cache refresh is OFF by default: `apt update` refreshes EVERY repo in
# sources.list.d, and this host's upstream DNS forwarder is unreliable, so a full
# refresh routinely fails. The member packages are standard Ubuntu main and are
# already in the local cache. Set true (with `-e samba_member_apt_update_cache=true`)
# to force a refresh when DNS is healthy and you need newer versions.
samba_member_apt_update_cache: false
samba_member_packages:
- samba
- winbind
- libnss-winbind
- krb5-user # kinit/klist for the join + smoke tests
- smbclient
- ldb-tools
# --- Preserved shares ---------------------------------------------------------
# Custom shares carried over verbatim from the pre-join standalone smb.conf.
# [printers]/[print$] and the Cockpit `include` are emitted by the template itself.
samba_member_shares:
# Guests (e.g. WinPE) get read-only access; the authenticated AD user panxiao81
# can write (write list). `guest only` is intentionally NOT set — that would force
# EVERY session to guest and defeat the write list. force user keeps all files
# owned by the local panxiao81 (uid 1000) that owns /mnt/pool/win.
- name: win
options:
comment: Windows install media (guest RO; panxiao81 RW)
path: /mnt/pool/win
browseable: "yes"
read only: "yes"
guest ok: "yes"
# Domain-qualified: a LOCAL panxiao81 (uid 1000) also exists, and a bare name
# resolves to the local SID, which won't match the connected AD user's SID.
write list: 'DDUPAN\panxiao81'
force user: panxiao81
hosts allow: 192.168.10.0/24 127.0.0.1
create mask: "0644"
directory mask: "0755"
# Files are written 0644 (no Unix +x). Since Samba 4.0, "open for execution"
# is denied without the execute bit, which blocks running setup.exe/dism off the
# share from WinPE. Allow execution regardless of the mode bit.
acl allow execute always: "yes"
@@ -0,0 +1,22 @@
---
- name: restart winbind
ansible.builtin.systemd:
name: winbind
state: restarted
when: not ansible_check_mode # unit only exists after the package install (skipped under --check)
- name: restart smbd
ansible.builtin.systemd:
name: smbd
state: restarted
- name: restart nmbd
ansible.builtin.systemd:
name: nmbd
state: restarted
failed_when: false # nmbd may be masked/absent on some setups
- name: restart systemd-resolved
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
@@ -0,0 +1,13 @@
---
galaxy_info:
role_name: samba_member
description: >-
Join a host to the Samba AD domain as a member fileserver (security = ADS)
with winbind identity mapping, preserving its existing SMB shares.
license: MIT
min_ansible_version: "2.15"
platforms:
- name: Ubuntu
versions:
- noble
dependencies: []
@@ -0,0 +1,171 @@
---
# Join this host to the AD domain as a Samba member (fileserver).
# Idempotent: the actual `net ads join` runs only when `net ads testjoin` fails,
# so re-running the play against an already-joined member is a no-op there.
#
# Ordering matters: packages → krb5.conf → split-DNS (so realm SRV resolves) →
# smb.conf + nsswitch → join → start winbind. The join needs working realm DNS,
# a valid krb5.conf, and <5 min clock skew (verified separately) to reach the KDC.
- name: Assert required variables are set
ansible.builtin.assert:
that:
- samba_ad_realm | length > 0
- samba_ad_domain | length > 0
- samba_ad_dc_ip | length > 0
- samba_member_netbios_name | length > 0
- samba_member_netbios_name | length <= 15
- samba_ad_admin_password is defined
- samba_ad_admin_password | length >= 8
fail_msg: >-
Need samba_ad_realm/domain/dc_ip, a <=15 char samba_member_netbios_name,
and samba_ad_admin_password from the vault.
- name: Install Samba member + winbind packages
ansible.builtin.apt:
name: "{{ samba_member_packages }}"
state: present
update_cache: "{{ samba_member_apt_update_cache | bool }}"
register: apt_install
until: apt_install is succeeded
retries: 3
delay: 5
- name: Warn if the clock is not NTP-synchronised (Kerberos fails on >5min skew)
ansible.builtin.command: timedatectl show -p NTPSynchronized --value
register: ntp_synced
changed_when: false
failed_when: false
check_mode: false # read-only probe; must run even under --check
- name: Assert time is synchronised before attempting the Kerberos join
ansible.builtin.assert:
that: ntp_synced.stdout | trim == 'yes'
fail_msg: >-
Clock is not NTP-synchronised — the Kerberos join will fail on skew.
Fix time sync (systemd-timesyncd/chrony) first.
success_msg: "Clock is NTP-synchronised."
- name: Install the Kerberos client config for the realm
ansible.builtin.template:
src: krb5.conf.j2
dest: /etc/krb5.conf
mode: "0644"
backup: true
# --- Split-DNS: route the realm to the DC so SRV discovery works ----------------
- name: Ensure resolved.conf.d drop-in directory exists
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
mode: "0755"
when: samba_member_configure_split_dns | bool
- name: Route the AD realm to the DC via systemd-resolved (split-DNS)
ansible.builtin.copy:
dest: /etc/systemd/resolved.conf.d/ad-realm.conf
content: |
# MANAGED BY ANSIBLE (role: samba_member)
# Send {{ samba_ad_realm | lower }} lookups to the DC; everything else stays
# on the per-link resolver. Needed because the LAN router does not forward the realm.
[Resolve]
DNS={{ samba_ad_dc_ip }}
Domains=~{{ samba_ad_realm | lower }}
mode: "0644"
when: samba_member_configure_split_dns | bool
register: dns_dropin
- name: Restart systemd-resolved so realm DNS is live before the join
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
when: dns_dropin is changed
- name: Verify the realm's LDAP SRV record now resolves
ansible.builtin.command: "host -t SRV _ldap._tcp.{{ samba_ad_realm | lower }}."
register: srv_check
changed_when: false
retries: 5
delay: 2
until: "'has SRV record' in srv_check.stdout"
failed_when: "'has SRV record' not in srv_check.stdout"
when: not ansible_check_mode # drop-in isn't really written under --check, so skip the probe
# --- Samba config -------------------------------------------------------------
- name: Back up the existing (standalone) smb.conf once
ansible.builtin.copy:
src: /etc/samba/smb.conf
dest: /etc/samba/smb.conf.pre-ads
remote_src: true
force: false # never clobber the original backup on re-runs
mode: "0644"
failed_when: false # tolerate a missing original
- name: Deploy the AD-member smb.conf
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
mode: "0644"
validate: "testparm -s %s"
notify:
- restart smbd
- restart nmbd
- restart winbind
- name: Add winbind to NSS passwd/group (resolve AD owners; no PAM/login change)
ansible.builtin.lineinfile:
path: /etc/nsswitch.conf
regexp: '^{{ item }}:(?!.*winbind)(.*)$'
line: '{{ item }}:\1 winbind'
backrefs: true
loop:
- passwd
- group
# --- Join ---------------------------------------------------------------------
- name: Check whether the host is already joined
ansible.builtin.command: net ads testjoin
register: testjoin
changed_when: false
failed_when: false
check_mode: false # read-only; keep testjoin.rc defined under --check
- name: Join the Active Directory domain as a member
ansible.builtin.command:
argv:
- net
- ads
- join
- "-U"
- "administrator%{{ samba_ad_admin_password }}"
when: testjoin.rc != 0
no_log: true
register: ads_join
changed_when: "'Joined' in (ads_join.stdout | default(''))"
- name: Enable and (re)start winbind now that we are joined
ansible.builtin.systemd:
name: winbind
state: started
enabled: true
when: not ansible_check_mode # unit only exists once the package is really installed
- name: Ensure smbd/nmbd are enabled and running
ansible.builtin.systemd:
name: "{{ item }}"
state: started
enabled: true
loop:
- smbd
- nmbd
failed_when: false # nmbd optional
when: not ansible_check_mode
# Apply pending smbd/nmbd/winbind restarts from the smb.conf change.
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Smoke tests
ansible.builtin.import_tasks: verify.yml
when: not ansible_check_mode # nothing is really joined under --check
tags: [verify]
@@ -0,0 +1,42 @@
---
# Post-join smoke tests. Run standalone with:
# ansible-playbook join-member.yml --tags verify
- name: Secure channel to the DC is healthy (net ads testjoin)
ansible.builtin.command: net ads testjoin
register: v_testjoin
changed_when: false
failed_when: "'Join is OK' not in v_testjoin.stdout"
- name: winbind can reach the domain (wbinfo -p / --online-status)
ansible.builtin.command: wbinfo -P
register: v_wbping
changed_when: false
failed_when: v_wbping.rc != 0
- name: Domain users are enumerable via winbind
ansible.builtin.command: wbinfo -u
register: v_wbusers
changed_when: false
failed_when: v_wbusers.rc != 0
- name: AD Administrator resolves through NSS (winbind idmap works)
ansible.builtin.command: >-
getent passwd {{ 'administrator' if samba_member_use_default_domain
else samba_ad_domain ~ '\\administrator' }}
register: v_getent
changed_when: false
failed_when: v_getent.rc != 0
- name: smbd is serving our shares (guest listing includes [win])
ansible.builtin.command: smbclient -L localhost -N
register: v_shares
changed_when: false
failed_when: "'win' not in (v_shares.stdout | lower)"
- name: Report
ansible.builtin.debug:
msg: >-
Member join OK: secure channel up, winbind online, AD users resolve via NSS,
and smbd is serving shares. Resolved Administrator ->
{{ v_getent.stdout | default('n/a') }}
@@ -0,0 +1,5 @@
# MANAGED BY ANSIBLE (role: samba_member) — do not edit.
[libdefaults]
default_realm = {{ samba_ad_realm }}
dns_lookup_realm = false
dns_lookup_kdc = true
@@ -0,0 +1,72 @@
#
# smb.conf — Samba as an Active Directory DOMAIN MEMBER (fileserver).
# MANAGED BY ANSIBLE (role: samba_member). Manual edits WILL be overwritten.
# Converted from the previous standalone config, backed up to smb.conf.pre-ads.
#
[global]
# --- AD domain membership ---
workgroup = {{ samba_ad_domain }}
realm = {{ samba_ad_realm }}
security = ADS
netbios name = {{ samba_member_netbios_name }}
server string = %h server (Samba, Ubuntu)
# Authenticate against the DC; keep the machine-account keytab in sync.
kerberos method = secrets and keytab
winbind refresh tickets = yes
winbind use default domain = {{ 'yes' if samba_member_use_default_domain else 'no' }}
winbind offline logon = yes
winbind enum users = no
winbind enum groups = no
# --- ID mapping (algorithmic RID — no RFC2307 attrs required) ---
idmap config * : backend = tdb
idmap config * : range = {{ samba_member_idmap_default_range }}
idmap config {{ samba_ad_domain }} : backend = rid
idmap config {{ samba_ad_domain }} : range = {{ samba_member_idmap_domain_range }}
template shell = {{ samba_member_template_shell }}
template homedir = {{ samba_member_template_homedir }}
# --- logging ---
log file = /var/log/samba/log.%m
max log size = 1000
logging = file
panic action = /usr/share/samba/panic-action %d
# --- guest handling (kept for the guest [win] share) ---
map to guest = bad user
usershare allow guests = yes
# Cockpit-managed ZFS shares (file created on demand by Cockpit; skipped if absent).
include = /etc/cockpit/zfs/shares.conf
# ---------------------------------------------------------------------------
# Printer sharing (stock Ubuntu defaults, preserved)
# ---------------------------------------------------------------------------
[printers]
comment = All Printers
browseable = no
path = /var/tmp
printable = yes
guest ok = no
read only = yes
create mask = 0700
[print$]
comment = Printer Drivers
path = /var/lib/samba/printers
browseable = yes
read only = yes
guest ok = no
# ---------------------------------------------------------------------------
# Custom shares (preserved from the pre-join standalone config)
# ---------------------------------------------------------------------------
{% for s in samba_member_shares %}
[{{ s.name }}]
{% for k, v in s.options.items() %}
{{ k }} = {{ v }}
{% endfor %}
{% endfor %}