diff --git a/CHANGELOG.md b/CHANGELOG.md index dc86a3db..6264f468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:ad_integration**: New role. Joins a host to an Active Directory domain (`realm join` plus SSSD configuration) so AD users can log in and AD groups can be used for access control and `sudo`. It wraps the upstream Linux System Role, analogous to the `network` and `kernel_settings` roles. Time synchronization, DNS, and crypto policy stay with the dedicated LFOps roles. * **role:mariadb_server**: Add `mariadb_server__cnf_innodb_snapshot_isolation` variable (MariaDB 10.6+), defaulting to `'ON'`. ### Security diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 132f6645..433adee7 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -5,6 +5,7 @@ Which Ansible role is proven to run on which OS? | Role | Deb 12 | Deb 13 | RHEL 8 | RHEL 9 | RHEL 10 | Ubu 22.04 | Ubu 24.04 | Ubu 26.04 | Other | |---------------------------------------|:------:|:------:|:------:|:------:|:-------:|:---------:|:---------:|:---------:|----------------------------------------------| | acme_sh | x | x | x | x | x | (x) | x | (x) | | +| ad_integration | (x) | (x) | (x) | (x) | (x) | (x) | (x) | (x) | | | alternatives | x | x | x | x | (x) | x | x | (x) | | | ansible_init | | | | | | | | | Fedora 35+ | | apache_httpd | x | x | x | x | x | (x) | x | (x) | | diff --git a/playbooks/README.md b/playbooks/README.md index 1babb9b2..28d1abc5 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -20,6 +20,13 @@ Calls the following roles (in order): * [acme_sh](https://github.com/Linuxfabrik/lfops/tree/main/roles/acme_sh) +## ad_integration.yml + +Calls the following roles (in order): + +* [ad_integration](https://github.com/Linuxfabrik/lfops/tree/main/roles/ad_integration) + + ## alternatives.yml Calls the following roles (in order): diff --git a/playbooks/ad_integration.yml b/playbooks/ad_integration.yml new file mode 100644 index 00000000..4baf684a --- /dev/null +++ b/playbooks/ad_integration.yml @@ -0,0 +1,23 @@ +- name: 'Playbook linuxfabrik.lfops.ad_integration' + hosts: + - 'lfops_ad_integration' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.ad_integration' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/playbooks/all.yml b/playbooks/all.yml index 5d66638c..164f12bb 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -1,4 +1,5 @@ - import_playbook: 'acme_sh.yml' +- import_playbook: 'ad_integration.yml' - import_playbook: 'alternatives.yml' - import_playbook: 'ansible_init.yml' - import_playbook: 'apache_httpd.yml' diff --git a/roles/ad_integration/README.md b/roles/ad_integration/README.md new file mode 100644 index 00000000..3ce9eba3 --- /dev/null +++ b/roles/ad_integration/README.md @@ -0,0 +1,179 @@ +# Ansible Role linuxfabrik.lfops.ad_integration + +This role joins a host to an Active Directory domain. It is a thin wrapper around the [`fedora.linux_system_roles.ad_integration` role](https://github.com/linux-system-roles/ad_integration), the upstream Linux System Role that runs `realm join` and configures SSSD (or Winbind) for direct AD integration. Once joined, AD users can log in and AD groups can be used for access control and `sudo` rules. + + +*Available in the next LFOps release.* + + +## How the Role Behaves + +* The role calls `fedora.linux_system_roles.ad_integration`, which by default joins the realm with `realm join` using `adcli` and configures `sssd` as the client software. The join credentials are read from `ad_integration__login`. +* Joining is not fully idempotent: on an already-joined host `realm` reports that the host is a member and the role makes no changes, but set `ad_integration__force_rejoin__host_var` to `true` to leave and re-join. +* The join credentials are passed in cleartext to `realm join`. Store them in Bitwarden and inject them via the `linuxfabrik.lfops.bitwarden_item` lookup rather than committing them to the inventory. +* This role only performs the domain join and SSSD configuration. It does NOT manage time synchronization, crypto policies, DNS, or networking. The upstream role's `manage_timesync`, `manage_crypto_policies`, and `manage_dns` features are intentionally not exposed; cover these prerequisites with the dedicated LFOps roles (see Requirements). + + +## Requirements + +* An Active Directory account that is allowed to join computers to the domain. +* Time must be in sync with the Active Directory domain controllers (Kerberos tolerates only a small clock skew). +* DNS must resolve the AD domain and its `_ldap._tcp` / `_kerberos._tcp` SRV records. +* Red Hat 8 and newer disable RC4 out of the box. Enable AES encryption in Active Directory, or relax the crypto policy on the host. + +Manual steps: + +* Install the [Linux System Roles](https://linux-system-roles.github.io/) on the Ansible control node, for example by calling `ansible-galaxy collection install fedora.linux_system_roles`. +* Synchronize the clock by running the [linuxfabrik.lfops.chrony](https://github.com/Linuxfabrik/lfops/tree/main/roles/chrony) role. +* Point DNS at the AD domain controllers by running the [linuxfabrik.lfops.network](https://github.com/Linuxfabrik/lfops/tree/main/roles/network) role (or a local resolver such as [linuxfabrik.lfops.blocky](https://github.com/Linuxfabrik/lfops/tree/main/roles/blocky) / [linuxfabrik.lfops.bind](https://github.com/Linuxfabrik/lfops/tree/main/roles/bind)). +* Optional: relax the crypto policy by running the [linuxfabrik.lfops.crypto_policy](https://github.com/Linuxfabrik/lfops/tree/main/roles/crypto_policy) role when the domain still requires RC4. + + +## Tags + +`ad_integration` + +* Joins the host to the Active Directory domain and configures SSSD. +* Triggers: none. + + +## Mandatory Role Variables + +`ad_integration__realm` + +* The Active Directory realm (domain) name to join, for example `EXAMPLE.COM`. +* Type: String. + +`ad_integration__login` + +* Credentials of the Active Directory user used to join the domain. Integrates with the `linuxfabrik.lfops.bitwarden_item` lookup. +* Type: Dictionary. +* Subkeys: + + * `username`: + + * Mandatory. The Active Directory user used to join the domain. + * Type: String. + + * `password`: + + * Mandatory. The password of that user. + * Type: String. + +Example: +```yaml +# mandatory +ad_integration__realm: 'EXAMPLE.COM' +ad_integration__login: + username: 'Administrator' + password: 'linuxfabrik' +``` + + +## Optional Role Variables + +`ad_integration__auto_id_mapping__host_var` / `ad_integration__auto_id_mapping__group_var` + +* Generate UID/GID numbers automatically instead of reading them from the directory (RFC 2307). +* Type: Bool. +* Default: `true` + +`ad_integration__client_software__host_var` / `ad_integration__client_software__group_var` + +* Client software to use with Active Directory. +* Type: String. One of `sssd`, `winbind`. +* Default: `'sssd'` + +`ad_integration__computer_ou__host_var` / `ad_integration__computer_ou__group_var` + +* Distinguished name of the organizational unit in which to create the computer account. Relative to the Root DSE or a complete LDAP DN. +* Type: String. +* Default: `''` + +`ad_integration__force_rejoin__host_var` / `ad_integration__force_rejoin__group_var` + +* Leave the existing domain before performing the join. +* Type: Bool. +* Default: `false` + +`ad_integration__join_parameters__host_var` / `ad_integration__join_parameters__group_var` + +* Additional parameters appended to the `realm join` command, for example `--user-principal`. +* Type: String. +* Default: `''` + +`ad_integration__join_to_dc__host_var` / `ad_integration__join_to_dc__group_var` + +* Host name or IP address of the domain controller to join via directly. +* Type: String. +* Default: `''` + +`ad_integration__membership_software__host_var` / `ad_integration__membership_software__group_var` + +* Software used to join the realm. +* Type: String. One of `adcli`, `samba`. +* Default: `'adcli'` + +`ad_integration__sssd_settings__host_var` / `ad_integration__sssd_settings__group_var` + +* Settings to include in the `[sssd]` section of `sssd.conf`. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `key`: + + * Mandatory. The configuration name. + * Type: String. + + * `value`: + + * Mandatory. The configuration value. + * Type: String. + +`ad_integration__sssd_custom_settings__host_var` / `ad_integration__sssd_custom_settings__group_var` + +* Settings to include in the `[domain/]` section of `sssd.conf`. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `key`: + + * Mandatory. The configuration name. + * Type: String. + + * `value`: + + * Mandatory. The configuration value. + * Type: String. + +Example: +```yaml +# optional +ad_integration__auto_id_mapping__host_var: false +ad_integration__client_software__host_var: 'sssd' +ad_integration__computer_ou__host_var: 'OU=Linux,OU=Servers,DC=example,DC=com' +ad_integration__force_rejoin__host_var: false +ad_integration__join_parameters__host_var: '--user-principal=host/client.example.com@EXAMPLE.COM' +ad_integration__join_to_dc__host_var: 'dc01.example.com' +ad_integration__membership_software__host_var: 'adcli' +ad_integration__sssd_settings__host_var: + - key: 'default_domain_suffix' + value: 'example.com' +ad_integration__sssd_custom_settings__host_var: + - key: 'ad_gpo_access_control' + value: 'enforcing' + - key: 'use_fully_qualified_names' + value: 'false' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/ad_integration/defaults/main.yml b/roles/ad_integration/defaults/main.yml new file mode 100644 index 00000000..9a94a181 --- /dev/null +++ b/roles/ad_integration/defaults/main.yml @@ -0,0 +1,100 @@ +ad_integration__auto_id_mapping__dependent_var: '' +ad_integration__auto_id_mapping__group_var: '' +ad_integration__auto_id_mapping__host_var: '' +ad_integration__auto_id_mapping__role_var: true +ad_integration__auto_id_mapping__combined_var: '{{ + ad_integration__auto_id_mapping__host_var if (ad_integration__auto_id_mapping__host_var | string | length) else + ad_integration__auto_id_mapping__group_var if (ad_integration__auto_id_mapping__group_var | string | length) else + ad_integration__auto_id_mapping__dependent_var if (ad_integration__auto_id_mapping__dependent_var | string | length) else + ad_integration__auto_id_mapping__role_var + }}' + +ad_integration__client_software__dependent_var: '' +ad_integration__client_software__group_var: '' +ad_integration__client_software__host_var: '' +ad_integration__client_software__role_var: 'sssd' +ad_integration__client_software__combined_var: '{{ + ad_integration__client_software__host_var if (ad_integration__client_software__host_var | string | length) else + ad_integration__client_software__group_var if (ad_integration__client_software__group_var | string | length) else + ad_integration__client_software__dependent_var if (ad_integration__client_software__dependent_var | string | length) else + ad_integration__client_software__role_var + }}' + +ad_integration__computer_ou__dependent_var: '' +ad_integration__computer_ou__group_var: '' +ad_integration__computer_ou__host_var: '' +ad_integration__computer_ou__role_var: '' +ad_integration__computer_ou__combined_var: '{{ + ad_integration__computer_ou__host_var if (ad_integration__computer_ou__host_var | string | length) else + ad_integration__computer_ou__group_var if (ad_integration__computer_ou__group_var | string | length) else + ad_integration__computer_ou__dependent_var if (ad_integration__computer_ou__dependent_var | string | length) else + ad_integration__computer_ou__role_var + }}' + +ad_integration__force_rejoin__dependent_var: '' +ad_integration__force_rejoin__group_var: '' +ad_integration__force_rejoin__host_var: '' +ad_integration__force_rejoin__role_var: false +ad_integration__force_rejoin__combined_var: '{{ + ad_integration__force_rejoin__host_var if (ad_integration__force_rejoin__host_var | string | length) else + ad_integration__force_rejoin__group_var if (ad_integration__force_rejoin__group_var | string | length) else + ad_integration__force_rejoin__dependent_var if (ad_integration__force_rejoin__dependent_var | string | length) else + ad_integration__force_rejoin__role_var + }}' + +ad_integration__join_parameters__dependent_var: '' +ad_integration__join_parameters__group_var: '' +ad_integration__join_parameters__host_var: '' +ad_integration__join_parameters__role_var: '' +ad_integration__join_parameters__combined_var: '{{ + ad_integration__join_parameters__host_var if (ad_integration__join_parameters__host_var | string | length) else + ad_integration__join_parameters__group_var if (ad_integration__join_parameters__group_var | string | length) else + ad_integration__join_parameters__dependent_var if (ad_integration__join_parameters__dependent_var | string | length) else + ad_integration__join_parameters__role_var + }}' + +ad_integration__join_to_dc__dependent_var: '' +ad_integration__join_to_dc__group_var: '' +ad_integration__join_to_dc__host_var: '' +ad_integration__join_to_dc__role_var: '' +ad_integration__join_to_dc__combined_var: '{{ + ad_integration__join_to_dc__host_var if (ad_integration__join_to_dc__host_var | string | length) else + ad_integration__join_to_dc__group_var if (ad_integration__join_to_dc__group_var | string | length) else + ad_integration__join_to_dc__dependent_var if (ad_integration__join_to_dc__dependent_var | string | length) else + ad_integration__join_to_dc__role_var + }}' + +ad_integration__membership_software__dependent_var: '' +ad_integration__membership_software__group_var: '' +ad_integration__membership_software__host_var: '' +ad_integration__membership_software__role_var: 'adcli' +ad_integration__membership_software__combined_var: '{{ + ad_integration__membership_software__host_var if (ad_integration__membership_software__host_var | string | length) else + ad_integration__membership_software__group_var if (ad_integration__membership_software__group_var | string | length) else + ad_integration__membership_software__dependent_var if (ad_integration__membership_software__dependent_var | string | length) else + ad_integration__membership_software__role_var + }}' + +ad_integration__sssd_custom_settings__dependent_var: [] +ad_integration__sssd_custom_settings__group_var: [] +ad_integration__sssd_custom_settings__host_var: [] +ad_integration__sssd_custom_settings__role_var: [] +ad_integration__sssd_custom_settings__combined_var: '{{ ( + ad_integration__sssd_custom_settings__role_var + + ad_integration__sssd_custom_settings__dependent_var + + ad_integration__sssd_custom_settings__group_var + + ad_integration__sssd_custom_settings__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="key") + }}' + +ad_integration__sssd_settings__dependent_var: [] +ad_integration__sssd_settings__group_var: [] +ad_integration__sssd_settings__host_var: [] +ad_integration__sssd_settings__role_var: [] +ad_integration__sssd_settings__combined_var: '{{ ( + ad_integration__sssd_settings__role_var + + ad_integration__sssd_settings__dependent_var + + ad_integration__sssd_settings__group_var + + ad_integration__sssd_settings__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="key") + }}' diff --git a/roles/ad_integration/meta/argument_specs.yml b/roles/ad_integration/meta/argument_specs.yml new file mode 100644 index 00000000..177471b4 --- /dev/null +++ b/roles/ad_integration/meta/argument_specs.yml @@ -0,0 +1,238 @@ +argument_specs: + main: + options: + + ad_integration__auto_id_mapping__dependent_var: + type: 'raw' + required: false + default: '' + description: >- + Generate UID/GID numbers automatically instead of reading them + from the directory (RFC 2307). Dependent-role injection. + + ad_integration__auto_id_mapping__group_var: + type: 'raw' + required: false + default: '' + description: >- + Generate UID/GID numbers automatically instead of reading them + from the directory (RFC 2307). Group-level override. + + ad_integration__auto_id_mapping__host_var: + type: 'raw' + required: false + default: '' + description: >- + Generate UID/GID numbers automatically instead of reading them + from the directory (RFC 2307). Host-level override. + + ad_integration__client_software__dependent_var: + type: 'str' + required: false + default: '' + description: >- + Client software to use with Active Directory (`sssd` or + `winbind`). Dependent-role injection. + + ad_integration__client_software__group_var: + type: 'str' + required: false + default: '' + description: >- + Client software to use with Active Directory (`sssd` or + `winbind`). Group-level override. + + ad_integration__client_software__host_var: + type: 'str' + required: false + default: '' + description: >- + Client software to use with Active Directory (`sssd` or + `winbind`). Host-level override. + + ad_integration__computer_ou__dependent_var: + type: 'str' + required: false + default: '' + description: >- + Distinguished name of the organizational unit for the computer + account. Dependent-role injection. + + ad_integration__computer_ou__group_var: + type: 'str' + required: false + default: '' + description: >- + Distinguished name of the organizational unit for the computer + account. Group-level override. + + ad_integration__computer_ou__host_var: + type: 'str' + required: false + default: '' + description: >- + Distinguished name of the organizational unit for the computer + account. Host-level override. + + ad_integration__force_rejoin__dependent_var: + type: 'raw' + required: false + default: '' + description: >- + Leave the existing domain before performing the join. + Dependent-role injection. + + ad_integration__force_rejoin__group_var: + type: 'raw' + required: false + default: '' + description: >- + Leave the existing domain before performing the join. + Group-level override. + + ad_integration__force_rejoin__host_var: + type: 'raw' + required: false + default: '' + description: >- + Leave the existing domain before performing the join. + Host-level override. + + ad_integration__join_parameters__dependent_var: + type: 'str' + required: false + default: '' + description: >- + Additional parameters appended to the `realm join` command. + Dependent-role injection. + + ad_integration__join_parameters__group_var: + type: 'str' + required: false + default: '' + description: >- + Additional parameters appended to the `realm join` command. + Group-level override. + + ad_integration__join_parameters__host_var: + type: 'str' + required: false + default: '' + description: >- + Additional parameters appended to the `realm join` command. + Host-level override. + + ad_integration__join_to_dc__dependent_var: + type: 'str' + required: false + default: '' + description: >- + Host name or IP address of the domain controller to join via + directly. Dependent-role injection. + + ad_integration__join_to_dc__group_var: + type: 'str' + required: false + default: '' + description: >- + Host name or IP address of the domain controller to join via + directly. Group-level override. + + ad_integration__join_to_dc__host_var: + type: 'str' + required: false + default: '' + description: >- + Host name or IP address of the domain controller to join via + directly. Host-level override. + + ad_integration__login: + type: 'dict' + required: true + description: >- + Credentials of the Active Directory user used to join the domain. + Expected subkeys: `username`, `password`. Integrates with the + `linuxfabrik.lfops.bitwarden_item` lookup. + + ad_integration__membership_software__dependent_var: + type: 'str' + required: false + default: '' + description: >- + Software used to join the realm (`adcli` or `samba`). + Dependent-role injection. + + ad_integration__membership_software__group_var: + type: 'str' + required: false + default: '' + description: >- + Software used to join the realm (`adcli` or `samba`). + Group-level override. + + ad_integration__membership_software__host_var: + type: 'str' + required: false + default: '' + description: >- + Software used to join the realm (`adcli` or `samba`). + Host-level override. + + ad_integration__realm: + type: 'str' + required: true + description: 'Active Directory realm (domain) name to join.' + + ad_integration__sssd_custom_settings__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[domain/]` section of `sssd.conf` + (`key`/`value`). Dependent-role injection. + + ad_integration__sssd_custom_settings__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[domain/]` section of `sssd.conf` + (`key`/`value`). Group-level override. + + ad_integration__sssd_custom_settings__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[domain/]` section of `sssd.conf` + (`key`/`value`). Host-level override. + + ad_integration__sssd_settings__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[sssd]` section of `sssd.conf` (`key`/`value`). + Dependent-role injection. + + ad_integration__sssd_settings__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[sssd]` section of `sssd.conf` (`key`/`value`). + Group-level override. + + ad_integration__sssd_settings__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: >- + Settings for the `[sssd]` section of `sssd.conf` (`key`/`value`). + Host-level override. diff --git a/roles/ad_integration/tasks/main.yml b/roles/ad_integration/tasks/main.yml new file mode 100644 index 00000000..baf32431 --- /dev/null +++ b/roles/ad_integration/tasks/main.yml @@ -0,0 +1,47 @@ +- block: + + - name: 'Assert that the join credentials are provided' + ansible.builtin.assert: + that: + - 'ad_integration__login["username"] is defined' + - 'ad_integration__login["password"] is defined' + fail_msg: 'ad_integration__login must define the "username" and "password" subkeys.' + quiet: true + + tags: + - 'always' + - 'ad_integration' + + +- block: + + - ansible.builtin.debug: + msg: + - 'Realm: {{ ad_integration__realm }}' + - 'Join user: {{ ad_integration__login["username"] }}' + - 'Client software: {{ ad_integration__client_software__combined_var }}' + - 'Membership software: {{ ad_integration__membership_software__combined_var }}' + - 'Combined sssd_settings:' + - '{{ ad_integration__sssd_settings__combined_var }}' + - 'Combined sssd_custom_settings:' + - '{{ ad_integration__sssd_custom_settings__combined_var }}' + + - name: 'Use fedora.linux_system_roles.ad_integration role to join the Active Directory domain' + ansible.builtin.include_role: + name: 'fedora.linux_system_roles.ad_integration' + vars: + ad_integration_realm: '{{ ad_integration__realm }}' # noqa var-naming[pattern] + ad_integration_user: '{{ ad_integration__login["username"] }}' # noqa var-naming[pattern] + ad_integration_password: '{{ ad_integration__login["password"] }}' # noqa var-naming[pattern] + ad_integration_auto_id_mapping: '{{ ad_integration__auto_id_mapping__combined_var | bool }}' # noqa var-naming[pattern] + ad_integration_client_software: '{{ ad_integration__client_software__combined_var }}' # noqa var-naming[pattern] + ad_integration_computer_ou: '{{ ad_integration__computer_ou__combined_var }}' # noqa var-naming[pattern] + ad_integration_force_rejoin: '{{ ad_integration__force_rejoin__combined_var | bool }}' # noqa var-naming[pattern] + ad_integration_join_parameters: '{{ ad_integration__join_parameters__combined_var }}' # noqa var-naming[pattern] + ad_integration_join_to_dc: '{{ ad_integration__join_to_dc__combined_var }}' # noqa var-naming[pattern] + ad_integration_membership_software: '{{ ad_integration__membership_software__combined_var }}' # noqa var-naming[pattern] + ad_integration_sssd_custom_settings: '{{ ad_integration__sssd_custom_settings__combined_var }}' # noqa var-naming[pattern] + ad_integration_sssd_settings: '{{ ad_integration__sssd_settings__combined_var }}' # noqa var-naming[pattern] + + tags: + - 'ad_integration'