Why do anything manually when you can use Ansible?
As I wrote here, I created an Active Directory domain and joined a single test VM to the domain.
Now I'll be writing about automating this process with ansible so that I can configure all my machines effortlessly.
Goals of this project
As with the last post, I want to join the servers to AD and then enable SSH authentication with public keys though altSecurityIdentities
.
I currently run two types of Linux servers. RHEL 8 and Ubuntu 20.04 LTS. Because of that, there are a couple of differences when configuring the setup. The most obvious one is that some of the packages are named differently. Some lesser-known differences are that RHEL comes with the authselect
utility but Ubuntu does not. Another one is that the sftp
binary is located in different paths and that causes the differences in sshd_config
.
Because of these discrepancies, I'm going to implement ansible roles for both operating systems.
Automation Outline
Let's go through the steps that will be written as ansible tasks.
- Check & install needed packages
- Configure Kerberos
- Join the domain
- Create a new user group named
ADMIN_hostname
- Configure SSSD, sshd and sudoers
- Configure PAM
- Enable & restart systemd services
Information || Variables required
Let's also list all the information that we need to define in the playbook.
- Active Directory Domain
- Active Directory Admin Credentials
- OS-Specific:
sftp
binary location- Packages to install
The active directory information is not something that changes much. Admin Credentials should be encrypted inside an Ansible Vault.
Variables and Roles
So let's write our first lines of the playbook! I'm using the hosts virtual
as that contains my Ansible-managed virtual machines.
- hosts: virtual
become: true
vars:
AD_Domain: AD.SORSA.CLOUD
AD_Domain_alt: ad.sorsa.cloud
Join_OU: DC=ad,DC=sorsa,DC=cloud
SRV_ADM_GRP_OU: OU=groups,DC=ad,DC=sorsa,DC=cloud
I've added become: true
as all of the tasks are going to require elevated privileges.
For variables, I have defined several variables that are essentially the same domain. Perhaps one could template these all but I don't think that would be very sane.
Creating an Active Directory role
Now we should handle the AD admin credentials. Let's create a new role with the following structure:
roles/
└── active_directory
├── files
├── meta
├── tasks
│ └── main.yml
├── templates
└── vars
└── credentials.yml
Let's first create credentials.yml
:
Next, we can encrypt this file with ansible-vault encrypt credentials.yml
. After this, you can use either --vault-pass
or --ask-vault-pass
to unlock the vault for the play.
Now create main.yml
:
This loads the credentials variables whenever the active_directory role is defined.
Creating the OS-Specific roles
After creating the AD role, we can now create a role for each OS Family.
roles/
├── rhel
│ ├── tasks
│ │ └── main.yml
│ └── vars
│ ├── ad-packages.yml
│ └── main.yml
└── ubuntu
├── tasks
│ └── main.yml
└── vars
├── ad-packages.yml
└── main.yml
These two roles are identical except for the hardcoded variables in vars/
. Let's start with vars/main.yml
:
Here are the ad-packages.yml
for both OSes:
Now let's add some include logic! This is the tasks/main.yml
file:
First, we include the main.yml file explicitly. Variables defined in vars/main.yml are available automatically for role-specific plays but to access it from our main playbook we need to include it with include_vars
.
Then we conditionally include_vars
the ad-packages.yml
, if the role active_directory
is loaded too.
Loading the correct OS role with ansible_os_family
Now we edit (or create) a role named common
so that the tasks/main.yml
contains this:
- include_role:
name: ubuntu
when: ansible_os_family == 'Debian'
- include_role:
name: rhel
when: ansible_os_family == 'RedHat'
Now depending on the current OS Family, the correct role is loaded. Great! Let's include these new roles into our playbook:
- hosts: virtual
roles:
- common
- active_directory
become: true
vars:
AD_Domain: AD.SORSA.CLOUD
AD_Domain_alt: ad.sorsa.cloud
Join_OU: DC=ad,DC=sorsa,DC=cloud
SRV_ADM_GRP_OU: OU=groups,DC=ad,DC=sorsa,DC=cloud
username: "{{ ad_admin_username }}"
password: "{{ ad_admin_password }}"
ssh_sftp_location: "{{ sftp_server_location }}"
Templates
Before we define our tasks, let's go over all the templates that we need:
- Kerberos: krb5.j2
- SSSD: sssd.conf.j2
- SSHD: sshdconfig.j2
- Sudoers: ADsudoers.j2
I'm placing these into a templates
folder in the ansible folder base. You should probably modify these templates as you see fit.
Lots of templating. This configures only a single realm but has an includedir
on the first line so that drop-in files can be added.
This is a pretty basic SSSD config, mainly templated for the domain. Do note the ldap_user_ssh_public_key
!
A small template that can be placed to /etc/sudoers.d/
.
Do note that this sshd configuration is pretty strict, it disables root login and password login. Note how AuthorizedKeysCommand
uses sss_ssh_authorizedkeys
.
Playbook Tasks
The first thing to do is install all the packages. This is easy:
- name: Checking if packages required to join AD realm are present
package:
name: "{{ active_directory_pkgs }}"
state: latest
Now we configure Kerberos so that the joining succeeds. As we defined includedir /etc/krb5.conf.d/
in the template, let's also ensure the folder exists:
- name: Configuring krb5.conf
template:
src: krb5.j2
dest: /etc/krb5.conf
owner: root
group: root
mode: 0644
- name: Create /etc/krb5.conf.d
file:
path: /etc/krb5.conf.d
state: directory
owner: root
group: root
mode: 755
Next up is joining the domain! I'd rather not use the shell module but didn't find a better way to do this yet. We pipe the password to stdin and then run adcli
with the correct parameters.
- name: Joining the AD realm (creating AD computer account and updating /etc/krb5.keytab)
shell: echo '{{ password }}' | adcli join --stdin-password {{ AD_Domain }} -U {{ username }} --domain-ou={{ Join_OU }}
Creating the admin group is similar. The only thing we add is a failed_when
because adcli
returns code 5 if the group already exists.
- name: Creating AD server admin group
shell: echo '{{ password }}' | adcli create-group ADMIN_{{ ansible_hostname }} --stdin-password --domain={{ AD_Domain }} --description="Admin group for {{ ansible_hostname }} server" --domain-ou={{ SRV_ADM_GRP_OU }} -U {{ username }}
register: group_result
failed_when: group_result.rc != 0 and group_result.rc != 5
Now let's do the templating, pretty straightforward.
- name: Configuring sssd.conf
template:
src: sssd.conf.j2
dest: /etc/sssd/sssd.conf
owner: root
group: root
mode: 0600
- name: Configuring sudoers
template:
src: ADsudoers.j2
dest: /etc/sudoers.d/ADsudoers
owner: root
group: root
mode: 0440
- name: Configuring /etc/ssh/sshd_config
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: 0600
Next up we need to do some conditioning based on the distro. As RHEL comes with the awesome authselect
utility, I'm using that. On Ubuntu that is not available so I want to enable mkhomedir
with pam-auth-update
.
- name: Configuring PAM/SSHD to use SSSD on RHEL
shell: authselect select sssd with-mkhomedir --force
when: ansible_os_family == 'RedHat'
- name: Configuring PAM to enable mkhomedir on Ubuntu
shell: pam-auth-update --enable mkhomedir
when: ansible_os_family == 'Debian'
Now the last thing to do is enable and set the state of all the services we need!
- name: Enabling oddjobd service
systemd:
name: oddjobd.service
enabled: yes
state: started
- name: Restarting SSSD
systemd:
name: sssd
enabled: yes
state: restarted
- name: Restarting SSHD
systemd:
name: sshd
enabled: yes
state: restarted
And here is the playbook in its entirety:
- hosts: virtual
roles:
- common
- active_directory
become: true
vars:
AD_Domain: AD.SORSA.CLOUD
AD_Domain_alt: ad.sorsa.cloud
Join_OU: DC=ad,DC=sorsa,DC=cloud
SRV_ADM_GRP_OU: OU=groups,DC=ad,DC=sorsa,DC=cloud
username: "{{ ad_admin_username }}"
password: "{{ ad_admin_password }}"
ssh_sftp_location: "{{ sftp_server_location }}"
tasks:
- name: Checking if packages required to join AD realm are present
package:
name: "{{ active_directory_pkgs }}"
state: latest
- name: Configuring krb5.conf
template:
src: krb5.j2
dest: /etc/krb5.conf
owner: root
group: root
mode: 0644
- name: Create /etc/krb5.conf.d
file:
path: /etc/krb5.conf.d
state: directory
owner: root
group: root
mode: 755
- name: Joining the AD realm (creating AD computer account and updating /etc/krb5.keytab)
shell: echo '{{ password }}' | adcli join --stdin-password {{ AD_Domain }} -U {{ username }} --domain-ou={{ Join_OU }}
- name: Creating AD server admin group
shell: echo '{{ password }}' | adcli create-group ADMIN_{{ ansible_hostname }} --stdin-password --domain={{ AD_Domain }} --description="Admin group for {{ ansible_hostname }} server" --domain-ou={{ SRV_ADM_GRP_OU }} -U {{ username }}
register: group_result
failed_when: group_result.rc != 0 and group_result.rc != 5
- name: Configuring sssd.conf
template:
src: sssd.conf.j2
dest: /etc/sssd/sssd.conf
owner: root
group: root
mode: 0600
- name: Configuring sudoers
template:
src: ADsudoers.j2
dest: /etc/sudoers.d/ADsudoers
owner: root
group: root
mode: 0440
- name: Configuring /etc/ssh/sshd_config
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: 0600
- name: Configuring PAM/SSHD to use SSSD on RHEL
shell: authselect select sssd with-mkhomedir --force
when: ansible_os_family == 'RedHat'
- name: Configuring PAM to enable mkhomedir on Ubuntu
shell: pam-auth-update --enable mkhomedir
when: ansible_os_family == 'Debian'
- name: Enabling oddjobd service
systemd:
name: oddjobd.service
enabled: yes
state: started
- name: Restarting SSSD
systemd:
name: sssd
enabled: yes
state: restarted
- name: Restarting SSHD
systemd:
name: sshd
enabled: yes
state: restarted
Results & Conclusion
After running this playbook all the machines in the inventory are successfully in my AD domain. Woohoo! Also logging in with the AD public key over ssh works great.
I love the flexibility and readability that roles add to ansible playbooks. I will definitely be using the OS-specific roles more in the future!