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,61 @@
---
# Service accounts + RBAC groups that downstream apps depend on.
# Codified because these were originally created by hand (svc-authelia), which
# means a DC rebuild would silently lose every app's ability to bind.
- name: Create service accounts
ansible.builtin.command:
argv:
- samba-tool
- user
- create
- "{{ item.name }}"
- "{{ item.password }}"
- "--description={{ item.description | default('') }}"
loop: "{{ samba_ad_service_accounts | default([]) }}"
loop_control:
label: "{{ item.name }}"
register: _svc_create
changed_when: "'created successfully' in (_svc_create.stdout | default(''))"
failed_when:
- _svc_create.rc != 0
- "'already exists' not in (_svc_create.stdout | default('') + _svc_create.stderr | default(''))"
no_log: true # passwords are on the argv
- name: Make service-account passwords non-expiring
# A bind account whose password silently expires takes the dependent app down
# with it, with no obvious cause. These are long random secrets in vault.yml.
ansible.builtin.command:
cmd: "samba-tool user setexpiry {{ item.name }} --noexpiry"
loop: "{{ samba_ad_service_accounts | default([]) }}"
loop_control:
label: "{{ item.name }}"
register: _svc_expiry
changed_when: _svc_expiry.rc == 0
- name: Create RBAC groups
ansible.builtin.command:
cmd: "samba-tool group add {{ item.name }}"
loop: "{{ samba_ad_groups | default([]) }}"
loop_control:
label: "{{ item.name }}"
register: _grp_create
changed_when: "'added successfully' in (_grp_create.stdout | default(''))"
failed_when:
- _grp_create.rc != 0
- "'already exists' not in (_grp_create.stdout | default('') + _grp_create.stderr | default(''))"
- name: Add group members
ansible.builtin.command:
cmd: "samba-tool group addmembers {{ item.name }} {{ item.members | join(',') }}"
loop: "{{ samba_ad_groups | default([]) | selectattr('members', 'defined') | list }}"
loop_control:
label: "{{ item.name }}"
register: _grp_members
changed_when: "'Added members' in (_grp_members.stdout | default(''))"
failed_when:
- _grp_members.rc != 0
# samba-tool wording varies: "already a member" for users, but
# "Attribute member already exists" when the member is a nested GROUP.
- "'already a member' not in (_grp_members.stdout | default('') + _grp_members.stderr | default('')) | lower"
- "'already exists' not in (_grp_members.stdout | default('') + _grp_members.stderr | default('')) | lower"
@@ -0,0 +1,32 @@
---
# Legacy client support — Win9x / NT4 / Win2000 / XP.
# INSECURE: re-enables SMB1, NTLMv1 and LANMAN auth. Only run on an isolated network.
# Gated by samba_ad_legacy_clients (default false).
- name: Inject legacy protocol settings into smb.conf [global]
ansible.builtin.blockinfile:
path: /etc/samba/smb.conf
marker: "\t# {mark} ANSIBLE MANAGED — legacy clients (INSECURE)"
insertafter: '^\[global\]'
block: |2
server min protocol = NT1
ntlm auth = ntlmv1-permitted
lanman auth = yes
client lanman auth = yes
allow nt4 crypto = yes
wins support = yes
notify: restart samba-ad-dc
- name: Allow weak Kerberos crypto for Windows 2000 (DES enctypes)
ansible.builtin.lineinfile:
path: /etc/krb5.conf
insertafter: '^\[libdefaults\]'
line: " allow_weak_crypto = true"
state: present
notify: restart samba-ad-dc
- name: Legacy warning
ansible.builtin.debug:
msg: >-
Legacy client support ENABLED (SMB1/NTLMv1/LANMAN). This materially weakens the
domain — keep retro machines on an isolated VLAN. See README "Retro clients".
@@ -0,0 +1,217 @@
---
# Provision a Samba Active Directory Domain Controller.
# Idempotent: the provision step is guarded by the existence of the sam.ldb database,
# so re-running the playbook against an already-provisioned DC is a no-op there.
#
# DNS ordering is deliberate: the box keeps using its normal upstream resolver for
# apt + provisioning, and is only repointed at its OWN Samba DNS *after* samba-ad-dc
# is up and serving :53. That way an interruption can never strand the DC on dead DNS.
- 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_ad_admin_password is defined
- samba_ad_admin_password | length >= 8
fail_msg: >-
Set samba_ad_realm/domain/dc_ip and provide samba_ad_admin_password from vault.
- name: Set hostname to the DC FQDN
ansible.builtin.hostname:
name: "{{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }}"
- name: Pin DC FQDN to its real IP in /etc/hosts (never 127.0.1.1)
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '\s{{ samba_ad_dc_hostname }}\.{{ samba_ad_realm | lower | regex_escape }}\b'
line: "{{ samba_ad_dc_ip }} {{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }} {{ samba_ad_dc_hostname }}"
state: present
- name: Install Samba AD DC packages
ansible.builtin.apt:
name: "{{ samba_ad_packages }}"
state: present
update_cache: true
- name: Ensure time sync is active (Kerberos dies on >5min skew)
ansible.builtin.service:
name: chrony
state: started
enabled: true
# --- Stop conflicting daemons --------------------------------------------------
- name: Disable distro smbd/nmbd/winbind (AD DC uses the unified samba service)
ansible.builtin.systemd:
name: "{{ item }}"
state: stopped
enabled: false
masked: false
loop:
- smbd
- nmbd
- winbind
failed_when: false
- name: Unmask samba-ad-dc service
ansible.builtin.systemd:
name: samba-ad-dc
masked: false
# --- Provision the domain (guarded) --------------------------------------------
- name: Check whether the domain is already provisioned
ansible.builtin.stat:
path: /var/lib/samba/private/sam.ldb
register: samba_sam_db
- name: Move stock smb.conf aside before first provision
ansible.builtin.command:
cmd: mv /etc/samba/smb.conf /etc/samba/smb.conf.orig
removes: /etc/samba/smb.conf
when: not samba_sam_db.stat.exists
- name: Provision the Active Directory domain
ansible.builtin.command:
argv:
- samba-tool
- domain
- provision
- "--use-rfc2307"
- "--realm={{ samba_ad_realm }}"
- "--domain={{ samba_ad_domain }}"
- "--server-role=dc"
- "--dns-backend=SAMBA_INTERNAL"
- "--adminpass={{ samba_ad_admin_password }}"
- "--option=dns forwarder = {{ samba_ad_dns_forwarder }}"
creates: /var/lib/samba/private/sam.ldb
no_log: true # keep the admin password out of logs
- name: Install the generated krb5.conf system-wide
ansible.builtin.copy:
src: /var/lib/samba/private/krb5.conf
dest: /etc/krb5.conf
remote_src: true
mode: "0644"
- name: Replace default LDAPS cert (positive serial + SANs) for modern Go clients
ansible.builtin.import_tasks: tls.yml
# --- Free port 53, then bring Samba's internal DNS online ----------------------
# Do this only now: up to here the box still resolves via its upstream (DHCP/cloud-init)
# resolver, so apt + provision above always had working DNS.
- name: Ensure resolved.conf.d drop-in directory exists
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
mode: "0755"
- name: Disable systemd-resolved stub listener (frees :53 for Samba)
ansible.builtin.copy:
dest: /etc/systemd/resolved.conf.d/no-stub.conf
content: |
[Resolve]
DNSStubListener=no
mode: "0644"
register: stub_dropin
- name: Restart systemd-resolved to release :53 before Samba binds it
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
when: stub_dropin is changed
- name: Enable and start samba-ad-dc
ansible.builtin.systemd:
name: samba-ad-dc
state: started
enabled: true
- name: Point the DC at its own Samba DNS (now that it is serving :53)
ansible.builtin.copy:
dest: /etc/resolv.conf
content: |
nameserver {{ samba_ad_dc_ip }}
search {{ samba_ad_realm | lower }}
follow: false
force: true
mode: "0644"
- name: Legacy client support (Win9x/NT4/2000/XP)
ansible.builtin.import_tasks: legacy.yml
when: samba_ad_legacy_clients | default(false) | bool
# --- Post-provision: reverse DNS zone (optional) -------------------------------
- name: Create reverse DNS zone
ansible.builtin.command:
cmd: >-
samba-tool dns zonecreate {{ samba_ad_dc_ip }} {{ samba_ad_reverse_zone }}
-U administrator%{{ samba_ad_admin_password }}
when: samba_ad_reverse_zone | length > 0
register: revzone
changed_when: "'already exists' not in (revzone.stderr | default(''))"
failed_when:
- revzone.rc != 0
- "'already exists' not in (revzone.stderr | default(''))"
no_log: true
- name: Register the DC's own PTR record in the reverse zone
ansible.builtin.command:
cmd: >-
samba-tool dns add {{ samba_ad_dc_ip }} {{ samba_ad_reverse_zone }}
{{ samba_ad_dc_ip.split('.')[3] }} PTR {{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }}.
-U administrator%{{ samba_ad_admin_password }}
when: samba_ad_reverse_zone | length > 0
register: ptr_add
changed_when: "'Record added successfully' in (ptr_add.stdout | default(''))"
failed_when:
- ptr_add.rc != 0
- "'already exists' not in (ptr_add.stderr | default('')) + (ptr_add.stdout | default(''))"
no_log: true
# --- KMS auto-activation records (_vlmcs SRV → vlmcsd) --------------------------
- name: Register the KMS host A record (SRV target)
ansible.builtin.command:
cmd: >-
samba-tool dns add {{ samba_ad_dc_ip }} {{ samba_ad_realm | lower }}
{{ samba_ad_kms_hostname }} A {{ samba_ad_kms_host_ip }}
-U administrator%{{ samba_ad_admin_password }}
when: samba_ad_kms_host_ip | length > 0
register: kms_a
changed_when: "'Record added successfully' in (kms_a.stdout | default(''))"
failed_when:
- kms_a.rc != 0
- "'already exists' not in (kms_a.stderr | default('')) + (kms_a.stdout | default(''))"
no_log: true
- name: Register the _vlmcs._tcp SRV record for KMS auto-discovery
ansible.builtin.command:
cmd: >-
samba-tool dns add {{ samba_ad_dc_ip }} {{ samba_ad_realm | lower }}
_vlmcs._tcp SRV "{{ samba_ad_kms_hostname }}.{{ samba_ad_realm | lower }} {{ samba_ad_kms_port }} 0 100"
-U administrator%{{ samba_ad_admin_password }}
when: samba_ad_kms_host_ip | length > 0
register: kms_srv
changed_when: "'Record added successfully' in (kms_srv.stdout | default(''))"
failed_when:
- kms_srv.rc != 0
- "'already exists' not in (kms_srv.stderr | default('')) + (kms_srv.stdout | default(''))"
no_log: true
# --- Extra A records for non-domain hosts (e.g. OpenBao) -----------------------
- name: Service accounts and RBAC groups
ansible.builtin.import_tasks: directory_objects.yml
tags: [directory, accounts]
- name: Register extra A records in the AD DNS zone
ansible.builtin.command:
cmd: >-
samba-tool dns add {{ samba_ad_dc_ip }} {{ samba_ad_realm | lower }}
{{ item.name }} A {{ item.ip }} -P
loop: "{{ samba_ad_extra_a_records }}"
register: extra_a
changed_when: "'Record added successfully' in (extra_a.stdout | default(''))"
failed_when:
- extra_a.rc != 0
- "'already exists' not in (extra_a.stderr | default('')) + (extra_a.stdout | default(''))"
tags: [dns]
@@ -0,0 +1,66 @@
---
# Replace Samba's default self-signed LDAPS certificate with one that has a POSITIVE
# serial number and proper SANs. Samba's auto-generated cert uses a negative serial,
# which Go 1.23+ LDAP clients (Authelia 4.39, etc.) reject at PARSE time with
# "x509: negative serial number" — so tls.skip_verify on the client can't help.
# Idempotent via community.crypto (only regenerates when inputs change).
- name: Ensure python cryptography is present (for community.crypto)
ansible.builtin.apt:
name: python3-cryptography
state: present
# --- Internal CA ---
- name: CA private key
community.crypto.openssl_privatekey:
path: /var/lib/samba/private/tls/ca-key.pem
size: 4096
mode: "0600"
- name: CA CSR (carries subject + CA basic constraints)
community.crypto.openssl_csr:
path: /var/lib/samba/private/tls/ca.csr
privatekey_path: /var/lib/samba/private/tls/ca-key.pem
common_name: "ddupan-ad-ca"
basic_constraints:
- "CA:TRUE"
basic_constraints_critical: true
use_common_name_for_san: false
- name: CA certificate (self-signed, positive serial)
community.crypto.x509_certificate:
path: /var/lib/samba/private/tls/ca.pem
csr_path: /var/lib/samba/private/tls/ca.csr
privatekey_path: /var/lib/samba/private/tls/ca-key.pem
provider: selfsigned
selfsigned_not_after: "+3650d"
mode: "0644"
notify: restart samba-ad-dc
# --- Server (LDAPS) cert signed by our CA ---
- name: Server private key
community.crypto.openssl_privatekey:
path: /var/lib/samba/private/tls/key.pem
size: 4096
mode: "0600"
- name: Server CSR (FQDN + SANs)
community.crypto.openssl_csr:
path: /var/lib/samba/private/tls/server.csr
privatekey_path: /var/lib/samba/private/tls/key.pem
common_name: "{{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }}"
subject_alt_name:
- "DNS:{{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }}"
- "DNS:{{ samba_ad_realm | lower }}"
- "IP:{{ samba_ad_dc_ip }}"
- name: Server certificate signed by our CA (positive serial)
community.crypto.x509_certificate:
path: /var/lib/samba/private/tls/cert.pem
csr_path: /var/lib/samba/private/tls/server.csr
ownca_path: /var/lib/samba/private/tls/ca.pem
ownca_privatekey_path: /var/lib/samba/private/tls/ca-key.pem
provider: ownca
ownca_not_after: "+3650d"
mode: "0644"
notify: restart samba-ad-dc
@@ -0,0 +1,36 @@
---
# Smoke tests — run via the `verify` tag: ansible-playbook provision-dc.yml --tags verify
# Fails the play if the DC is not answering LDAP/Kerberos/DNS/SMB correctly.
- name: LDAP / domain level responds
ansible.builtin.command: samba-tool domain level show
changed_when: false
- name: DNS SRV record for LDAP resolves
ansible.builtin.command: "host -t SRV _ldap._tcp.{{ samba_ad_realm | lower }}."
register: srv_ldap
changed_when: false
failed_when: "'has SRV record' not in srv_ldap.stdout"
- name: DNS SRV record for Kerberos resolves
ansible.builtin.command: "host -t SRV _kerberos._udp.{{ samba_ad_realm | lower }}."
register: srv_krb
changed_when: false
failed_when: "'has SRV record' not in srv_krb.stdout"
- name: DC A record resolves to its own IP
ansible.builtin.command: "host -t A {{ samba_ad_dc_hostname }}.{{ samba_ad_realm | lower }}."
register: dc_a
changed_when: false
failed_when: samba_ad_dc_ip not in dc_a.stdout
- name: SMB default shares are listable
ansible.builtin.command: "smbclient -L localhost -U administrator%{{ samba_ad_admin_password }}"
register: smb_list
changed_when: false
no_log: true
failed_when: "'sysvol' not in (smb_list.stdout | lower)"
- name: Report
ansible.builtin.debug:
msg: "Samba AD DC smoke tests passed: LDAP + Kerberos SRV + DC A record + SMB shares OK."