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,37 @@
|
||||
---
|
||||
# samba_ad_dc role defaults — override in group_vars/host_vars.
|
||||
# Secrets (samba_ad_admin_password) MUST come from an Ansible Vault file, not here.
|
||||
|
||||
samba_ad_realm: "AD.EXAMPLE.COM" # DNS realm, uppercase. Delegated subdomain you own.
|
||||
samba_ad_domain: "EXAMPLE" # NetBIOS / short name, <=15 chars, uppercase, no dots.
|
||||
samba_ad_dc_hostname: "dc1" # short hostname of this DC
|
||||
samba_ad_dc_ip: "10.10.10.10" # this DC's static IP (used for /etc/hosts + resolv.conf)
|
||||
samba_ad_dns_forwarder: "10.10.10.1" # where the DC forwards non-AD lookups
|
||||
|
||||
# Reverse DNS zone to create after provision (optional; empty string skips it).
|
||||
# Give the in-addr.arpa name directly, e.g. "10.10.10.in-addr.arpa" for 10.10.10.0/24.
|
||||
samba_ad_reverse_zone: ""
|
||||
|
||||
# Extra A records to publish in the AD DNS zone for non-domain hosts (e.g. OpenBao).
|
||||
# List of {name, ip}. Added with the DC machine account (-P) — no admin password.
|
||||
samba_ad_extra_a_records: []
|
||||
|
||||
# Re-enable legacy protocols (SMB1/NTLMv1/LANMAN) for Win9x/NT4/2000/XP. INSECURE.
|
||||
samba_ad_legacy_clients: false
|
||||
|
||||
# Package set (Debian/Ubuntu). RHEL-family names differ — adjust if you switch base.
|
||||
samba_ad_packages:
|
||||
- samba
|
||||
- krb5-config
|
||||
- krb5-user # kinit/klist for admin + smoke tests
|
||||
- winbind
|
||||
- smbclient
|
||||
- ldb-tools
|
||||
- chrony
|
||||
|
||||
# Distro daemons that MUST be stopped on an AD DC (the unified `samba` service owns these roles).
|
||||
samba_ad_conflicting_services:
|
||||
- smbd
|
||||
- nmbd
|
||||
- winbind
|
||||
- systemd-resolved # stub listener freed separately; service left running but stub disabled
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: restart systemd-resolved
|
||||
ansible.builtin.service:
|
||||
name: systemd-resolved
|
||||
state: restarted
|
||||
|
||||
- name: restart samba-ad-dc
|
||||
ansible.builtin.systemd:
|
||||
name: samba-ad-dc
|
||||
state: restarted
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: samba_ad_dc
|
||||
description: Provision a Samba Active Directory Domain Controller (Kerberos + LDAP + DNS + SMB).
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions: [bookworm]
|
||||
- name: Ubuntu
|
||||
versions: [jammy, noble]
|
||||
dependencies: []
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user