diff --git a/README.md b/README.md index 00b9992..40fbe9b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ad-join-script.sh b/ad-join-script.sh index ec1bfe7..e732c36 100644 --- a/ad-join-script.sh +++ b/ad-join-script.sh @@ -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..." diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..284160c --- /dev/null +++ b/ansible/README.md @@ -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`). diff --git a/ansible/roles/ad_join/defaults/main.yml b/ansible/roles/ad_join/defaults/main.yml new file mode 100644 index 0000000..c4c07e1 --- /dev/null +++ b/ansible/roles/ad_join/defaults/main.yml @@ -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 diff --git a/ansible/roles/ad_join/tasks/main.yml b/ansible/roles/ad_join/tasks/main.yml new file mode 100644 index 0000000..502d964 --- /dev/null +++ b/ansible/roles/ad_join/tasks/main.yml @@ -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')) diff --git a/ansible/site.yml b/ansible/site.yml new file mode 100644 index 0000000..535f693 --- /dev/null +++ b/ansible/site.yml @@ -0,0 +1,9 @@ +--- +- name: Join Ubuntu hosts to Active Directory + hosts: all + become: true + gather_facts: true + + roles: + - role: ad_join +