Add Ansible deployment and systemd-resolved DNS support

This commit is contained in:
jeanGaston 2026-01-09 19:19:11 +01:00
parent 605c112d8b
commit 36cf28ce46
6 changed files with 284 additions and 25 deletions

View File

@ -33,6 +33,31 @@ This script automates the process of joining an Ubuntu Server to an Active Direc
7. If the script completes successfully, your server will be joined to the Active Directory domain.
8. You might need to reboot for the authentication with AD credentials to work
### Non-interactive usage
You can also run the script without prompts by setting environment variables:
- `ADJOIN_HOSTNAME`
- `ADJOIN_ADMIN_USER`
- `ADJOIN_ADMIN_PASSWORD`
- `ADJOIN_DOMAIN_NAME`
- `ADJOIN_AD_GROUP`
- `ADJOIN_DNS_SERVERS` (preferred; comma/space separated)
- `ADJOIN_DNS_SERVER` (backward compatible single server)
- `ADJOIN_DNS_INTERFACE` (optional; interface name for `resolvectl`, e.g. `eth0`)
### DNS management note
This script needs to be able to set DNS for domain discovery/join. It tries `systemd-resolved` first, then falls back to writing `/etc/resolv.conf`.
If your DNS is managed by another component (e.g. NetworkManager, netplan, resolvconf, or cloud-init), you may need to adapt DNS configuration for your environment or the join can fail.
## Ansible
An Ansible role and example playbook are included under `ansible/` to deploy and run the script non-interactively (all inputs are passed as variables, suitable for Semaphore).
See `ansible/README.md` for required variables and usage.
## Disclaimer
This script is provided as-is and without any warranty. Use it at your own risk. Be sure to have valid credentials and administrative access to your Active Directory domain before running the script.

View File

@ -1,24 +1,110 @@
#!/bin/bash
# Prompt the user for the hostname
read -p "Enter the hostname for the server: " hostname
echo
set -euo pipefail
hostname="${ADJOIN_HOSTNAME:-}"
admin_user="${ADJOIN_ADMIN_USER:-}"
admin_password="${ADJOIN_ADMIN_PASSWORD:-}"
domain_name="${ADJOIN_DOMAIN_NAME:-}"
ad_group="${ADJOIN_AD_GROUP:-}"
dns_servers_raw="${ADJOIN_DNS_SERVERS:-${ADJOIN_DNS_SERVER:-}}"
dns_interface="${ADJOIN_DNS_INTERFACE:-}"
configure_dns() {
local domain="$1"
shift
local -a servers=("$@")
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet systemd-resolved; then
echo "Configuring DNS via systemd-resolved..."
sudo mkdir -p /etc/systemd/resolved.conf.d
{
echo "[Resolve]"
echo "DNS=${servers[*]}"
echo "Domains=$domain"
} | sudo tee /etc/systemd/resolved.conf.d/ad-join.conf >/dev/null
sudo systemctl restart systemd-resolved
return 0
fi
if command -v resolvectl >/dev/null 2>&1; then
local iface="$dns_interface"
if [ -z "$iface" ] && command -v ip >/dev/null 2>&1; then
iface="$(ip route show default 2>/dev/null | awk '{print $5; exit}')"
fi
if [ -n "$iface" ]; then
echo "Configuring DNS via resolvectl on interface: $iface"
sudo resolvectl dns "$iface" "${servers[@]}"
sudo resolvectl domain "$iface" "$domain"
return 0
fi
fi
echo "Configuring DNS via /etc/resolv.conf..."
{
for server in "${servers[@]}"; do
echo "nameserver $server"
done
} | sudo tee /etc/resolv.conf >/dev/null
}
if [ -z "$hostname" ]; then
read -p "Enter the hostname for the server: " hostname
echo
fi
# Prompt the user for the necessary information
read -p "Administrator username (AdminUser): " admin_user
read -s -p "Administrator password: " admin_password
echo # To move to the next line
read -p "Active Directory domain name: " domain_name
read -p "Active Directory group for sudo access: " ad_group
if [ -z "$admin_user" ]; then
read -p "Administrator username (AdminUser): " admin_user
fi
if [ -z "$admin_password" ]; then
read -s -p "Administrator password: " admin_password
echo # To move to the next line
fi
if [ -z "$domain_name" ]; then
read -p "Active Directory domain name: " domain_name
fi
if [ -z "$ad_group" ]; then
read -p "Active Directory group for sudo access: " ad_group
fi
# Prompt for DNS server IP and verify DNS resolution
while true; do
read -p "DNS server IP: " dns_server
if nslookup $domain_name $dns_server; then
break
else
echo "DNS resolution failed. Please enter a valid DNS server IP."
# Prompt for DNS server IP(s) and verify DNS resolution
if [ -z "$dns_servers_raw" ]; then
while true; do
read -p "DNS server IP(s) (comma or space separated): " dns_servers_raw
dns_servers_raw="${dns_servers_raw//,/ }"
read -r -a dns_servers <<<"$dns_servers_raw"
dns_ok=false
for server in "${dns_servers[@]}"; do
if nslookup "$domain_name" "$server" >/dev/null 2>&1; then
dns_ok=true
break
fi
done
if [ "$dns_ok" = true ]; then
break
fi
echo "DNS resolution failed using provided server(s). Please try again."
done
else
dns_servers_raw="${dns_servers_raw//,/ }"
read -r -a dns_servers <<<"$dns_servers_raw"
dns_ok=false
for server in "${dns_servers[@]}"; do
if nslookup "$domain_name" "$server" >/dev/null 2>&1; then
dns_ok=true
break
fi
done
if [ "$dns_ok" != true ]; then
echo "DNS resolution failed using provided server(s): ${dns_servers[*]}" >&2
exit 1
fi
done
fi
# Install the necessary packages with a loading bar
echo "Installing required packages..."
apt -y install realmd sssd sssd-tools libnss-sss libpam-sss adcli samba-common-bin oddjob oddjob-mkhomedir packagekit
@ -26,20 +112,16 @@ apt -y install realmd sssd sssd-tools libnss-sss libpam-sss adcli samba-common-b
echo "Changing the hostname to: $hostname.$domain_name"
# Change the hostname
hostnamectl set-hostname $hostname
hostnamectl set-hostname "$hostname.$domain_name"
echo "$hostname.$domain_name" | sudo tee /etc/hostname
# Change the DNS server settings in /etc/resolv.conf
echo "Changing DNS server to: $dns_server"
echo "nameserver $dns_server" | sudo tee /etc/resolv.conf
# Install the necessary packages with a loading bar
echo "Installing required packages..."
apt -y install realmd sssd sssd-tools libnss-sss libpam-sss adcli samba-common-bin oddjob oddjob-mkhomedir packagekit & loading_bar
# Change DNS settings
echo "Setting DNS server(s) to: ${dns_servers[*]}"
configure_dns "$domain_name" "${dns_servers[@]}"
# Discover the domain and join, registering DNS
echo "Joining the domain and registering DNS..."
echo $admin_password | realm join --user=$admin_user $domain_name
printf '%s\n' "$admin_password" | realm join --user="$admin_user" "$domain_name"
# Modify the sssd.conf configuration to enable dynamic DNS updates
echo "Configuring dynamic DNS updates..."

47
ansible/README.md Normal file
View File

@ -0,0 +1,47 @@
# Ansible deployment
This repo includes an Ansible role that deploys and runs `ad-join-script.sh` in a non-interactive way by passing all inputs as variables.
## Files
- `ansible/site.yml`: example playbook using the role
- `ansible/roles/ad_join`: role that copies and runs the script
## Variables
Required:
- `ad_join_hostname`: short hostname (without domain)
- `ad_join_domain_name`: AD domain (e.g. `corp.example.com`)
- `ad_join_admin_user`: account used to join the domain
- `ad_join_admin_password`: password for `ad_join_admin_user`
- `ad_join_dns_servers`: DNS server IPs used to validate domain resolution (list preferred; also accepts a comma-separated string)
- `ad_join_dns_server`: single DNS server IP (backward compatible)
- `ad_join_ad_group`: AD group to grant sudo access (written to `/etc/sudoers`)
Optional:
- `ad_join_force` (default: `false`): run even if `realm list` already shows the domain
- `ad_join_run` (default: `true`): set to `false` to only deploy the script
## Semaphore setup notes
- Point Semaphore to the playbook at `ansible/site.yml`.
- Define the variables above in the task template (use a secret variable for `ad_join_admin_password`).
## Run (example)
```bash
ansible-playbook -i inventory.ini ansible/site.yml \
-e ad_join_hostname=ubuntuhost01 \
-e ad_join_domain_name=corp.example.com \
-e ad_join_admin_user=JoinUser \
-e ad_join_admin_password='***' \
-e 'ad_join_dns_servers=["192.0.2.53","192.0.2.54"]' \
-e ad_join_ad_group='LinuxAdmins'
```
## Important behavior
- The script attempts to configure DNS via `systemd-resolved` first, then falls back to writing `/etc/resolv.conf`. If DNS is managed elsewhere, you may need to adapt DNS configuration for your environment.
- The role runs the join step only when the host is not already joined (unless `ad_join_force: true`).

View File

@ -0,0 +1,26 @@
---
ad_join_script_src: "{{ playbook_dir }}/../ad-join-script.sh"
ad_join_script_dest: /usr/local/sbin/ubuntu-ad-join.sh
ad_join_install_packages:
- realmd
- sssd
- sssd-tools
- libnss-sss
- libpam-sss
- adcli
- samba-common-bin
- oddjob
- oddjob-mkhomedir
- packagekit
ad_join_hostname: ""
ad_join_admin_user: ""
ad_join_admin_password: ""
ad_join_domain_name: ""
ad_join_ad_group: ""
ad_join_dns_server: ""
ad_join_dns_servers: []
ad_join_force: false
ad_join_run: true

View File

@ -0,0 +1,70 @@
---
- name: Validate required variables
ansible.builtin.assert:
that:
- ad_join_hostname | length > 0
- ad_join_domain_name | length > 0
- ad_join_admin_user | length > 0
- ad_join_admin_password | length > 0
- ad_join_ad_group | length > 0
fail_msg: >-
Missing required variables. Pass them as extra vars (Semaphore) or via inventory/group vars.
- name: Normalize DNS servers list
ansible.builtin.set_fact:
ad_join_dns_servers_effective: >-
{{
(
ad_join_dns_servers.split(',') | map('trim') | reject('equalto', '') | list
)
if (ad_join_dns_servers is string)
else (ad_join_dns_servers | default([]))
}}
- name: Back-compat for single DNS variable
ansible.builtin.set_fact:
ad_join_dns_servers_effective: "{{ [ad_join_dns_server] }}"
when:
- ad_join_dns_servers_effective | length == 0
- ad_join_dns_server | length > 0
- name: Validate DNS server(s) provided
ansible.builtin.assert:
that:
- ad_join_dns_servers_effective | length > 0
fail_msg: >-
Missing DNS server(s). Set ad_join_dns_servers (preferred) or ad_join_dns_server.
- name: Install required packages
ansible.builtin.apt:
name: "{{ ad_join_install_packages }}"
state: present
update_cache: true
- name: Check current domain join status
ansible.builtin.command: realm list
register: ad_join_realm_list
changed_when: false
failed_when: false
- name: Deploy AD join script
ansible.builtin.copy:
src: "{{ ad_join_script_src }}"
dest: "{{ ad_join_script_dest }}"
mode: "0750"
owner: root
group: root
- name: Join domain using script
ansible.builtin.command: "{{ ad_join_script_dest }}"
environment:
ADJOIN_HOSTNAME: "{{ ad_join_hostname }}"
ADJOIN_ADMIN_USER: "{{ ad_join_admin_user }}"
ADJOIN_ADMIN_PASSWORD: "{{ ad_join_admin_password }}"
ADJOIN_DOMAIN_NAME: "{{ ad_join_domain_name }}"
ADJOIN_AD_GROUP: "{{ ad_join_ad_group }}"
ADJOIN_DNS_SERVERS: "{{ ad_join_dns_servers_effective | join(' ') }}"
no_log: true
when:
- ad_join_run | bool
- ad_join_force | bool or (ad_join_realm_list.stdout is not regex_search('realm-name:\\s*' ~ (ad_join_domain_name | regex_escape) ~ '\\b'))

9
ansible/site.yml Normal file
View File

@ -0,0 +1,9 @@
---
- name: Join Ubuntu hosts to Active Directory
hosts: all
become: true
gather_facts: true
roles:
- role: ad_join