Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
@@ -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 %}
|
||||
Reference in New Issue
Block a user