From bd86a9e1f9f89b4428806700501578eb6eadae48 Mon Sep 17 00:00:00 2001 From: Ezri Brimhall Date: Thu, 5 Sep 2024 09:02:15 -0600 Subject: [PATCH] Initial work on PAM config. Has issues, deploy with care --- ansible.cfg | 3 + group_vars/all.yml | 8 ++ group_vars/desktop.yml | 5 + inventory | 15 +++ playbooks/deploy-desktop.yml | 8 ++ playbooks/deploy-kanidm-native.yml | 6 + playbooks/roles/2fa/files/common-account | 8 ++ playbooks/roles/2fa/files/common-auth | 7 ++ playbooks/roles/2fa/files/common-password | 6 + playbooks/roles/2fa/files/common-session | 6 + playbooks/roles/2fa/files/first-factor | 6 + .../roles/2fa/files/remote-switch.access.conf | 2 + playbooks/roles/2fa/files/system-auth | 6 + playbooks/roles/2fa/tasks/main.yml | 74 ++++++++++++ .../roles/2fa/templates/second-factor.j2 | 11 ++ playbooks/roles/aur/tasks/main.yml | 42 +++++++ .../roles/kanidm_native/files/common-account | 12 ++ .../roles/kanidm_native/files/common-password | 8 ++ .../roles/kanidm_native/files/common-session | 7 ++ .../roles/kanidm_native/files/first-factor | 11 ++ .../roles/kanidm_native/files/kanidm-hack.sh | 10 ++ .../roles/kanidm_native/handlers/main.yml | 6 + playbooks/roles/kanidm_native/tasks/main.yml | 109 ++++++++++++++++++ .../templates/10-kanidm-keys.conf.j2 | 6 + .../roles/kanidm_native/templates/config.j2 | 8 ++ .../roles/kanidm_native/templates/unixd.j2 | 11 ++ requirements.yml | 6 + 27 files changed, 407 insertions(+) create mode 100644 ansible.cfg create mode 100644 group_vars/all.yml create mode 100644 group_vars/desktop.yml create mode 100644 inventory create mode 100644 playbooks/deploy-desktop.yml create mode 100644 playbooks/deploy-kanidm-native.yml create mode 100644 playbooks/roles/2fa/files/common-account create mode 100644 playbooks/roles/2fa/files/common-auth create mode 100644 playbooks/roles/2fa/files/common-password create mode 100644 playbooks/roles/2fa/files/common-session create mode 100644 playbooks/roles/2fa/files/first-factor create mode 100644 playbooks/roles/2fa/files/remote-switch.access.conf create mode 100644 playbooks/roles/2fa/files/system-auth create mode 100644 playbooks/roles/2fa/tasks/main.yml create mode 100644 playbooks/roles/2fa/templates/second-factor.j2 create mode 100644 playbooks/roles/aur/tasks/main.yml create mode 100644 playbooks/roles/kanidm_native/files/common-account create mode 100644 playbooks/roles/kanidm_native/files/common-password create mode 100644 playbooks/roles/kanidm_native/files/common-session create mode 100644 playbooks/roles/kanidm_native/files/first-factor create mode 100644 playbooks/roles/kanidm_native/files/kanidm-hack.sh create mode 100644 playbooks/roles/kanidm_native/handlers/main.yml create mode 100644 playbooks/roles/kanidm_native/tasks/main.yml create mode 100644 playbooks/roles/kanidm_native/templates/10-kanidm-keys.conf.j2 create mode 100644 playbooks/roles/kanidm_native/templates/config.j2 create mode 100644 playbooks/roles/kanidm_native/templates/unixd.j2 create mode 100644 requirements.yml diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..dcb0621 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory +host_key_checking = False diff --git a/group_vars/all.yml b/group_vars/all.yml new file mode 100644 index 0000000..aecd31e --- /dev/null +++ b/group_vars/all.yml @@ -0,0 +1,8 @@ +--- + +# Default user source. Overridden in other groups. +user_source: "local" + +kanidm_uri: "https://idm.ezri.dev" + +kanidm_supplemental: [] diff --git a/group_vars/desktop.yml b/group_vars/desktop.yml new file mode 100644 index 0000000..cfdabf3 --- /dev/null +++ b/group_vars/desktop.yml @@ -0,0 +1,5 @@ +--- + +user_source: "kanidm-native" + +allowed_groups: "{{ [ 'desktop_users' ] }}" diff --git a/inventory b/inventory new file mode 100644 index 0000000..3c50a4f --- /dev/null +++ b/inventory @@ -0,0 +1,15 @@ +; -*-conf-windows-*- + +[container_runners] +horizon.servers.ezri.dev + +[kanidm_native] +normandy.network.ezri.dev +tycho.vpn.ezri.dev +rocinante.vpn.ezri.dev +serenity.wlan.ezri.dev + +[desktop] +;normandy ansible_connection=ssh ansible_become=true +serenity ansible_connection=ssh ansible_become=true + diff --git a/playbooks/deploy-desktop.yml b/playbooks/deploy-desktop.yml new file mode 100644 index 0000000..31c2683 --- /dev/null +++ b/playbooks/deploy-desktop.yml @@ -0,0 +1,8 @@ +--- + +- name: Configure desktop systems + hosts: desktop + roles: + - aur + - kanidm_native + - 2fa diff --git a/playbooks/deploy-kanidm-native.yml b/playbooks/deploy-kanidm-native.yml new file mode 100644 index 0000000..cbebcf6 --- /dev/null +++ b/playbooks/deploy-kanidm-native.yml @@ -0,0 +1,6 @@ +--- + +- name: Kanidm native unixd clients + hosts: kanidm_native + roles: + - kanidm_native diff --git a/playbooks/roles/2fa/files/common-account b/playbooks/roles/2fa/files/common-account new file mode 100644 index 0000000..e31fbcd --- /dev/null +++ b/playbooks/roles/2fa/files/common-account @@ -0,0 +1,8 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +-account [success=2 default=ignore] pam_systemd_home.so +account [success=1 default=ignore] pam_unix.so +# If any of the above account lines fail, they'll jump here, which kills the authorization attempt. +account [default=die] pam_deny.so +account optional pam_permit.so +account required pam_time.so diff --git a/playbooks/roles/2fa/files/common-auth b/playbooks/roles/2fa/files/common-auth new file mode 100644 index 0000000..8dbef0c --- /dev/null +++ b/playbooks/roles/2fa/files/common-auth @@ -0,0 +1,7 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 8 -*- + +auth include first-factor +auth include second-factor +auth optional pam_permit.so +auth required pam_env.so +auth required pam_faillock.so authsucc diff --git a/playbooks/roles/2fa/files/common-password b/playbooks/roles/2fa/files/common-password new file mode 100644 index 0000000..57a041b --- /dev/null +++ b/playbooks/roles/2fa/files/common-password @@ -0,0 +1,6 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +-password [success=2 default=ignore] pam_systemd_home.so +password [success=1 default=ignore] pam_unix.so try_first_pass nullok shadow sha512 +password [default=die] pam_deny.so +password optional pam_permit.so diff --git a/playbooks/roles/2fa/files/common-session b/playbooks/roles/2fa/files/common-session new file mode 100644 index 0000000..cdc4fa1 --- /dev/null +++ b/playbooks/roles/2fa/files/common-session @@ -0,0 +1,6 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +session required pam_limits.so +session [success=1 default=ignore] pam_unix.so +session [default=die] pam_deny.so +session optional pam_permit.so diff --git a/playbooks/roles/2fa/files/first-factor b/playbooks/roles/2fa/files/first-factor new file mode 100644 index 0000000..53c6415 --- /dev/null +++ b/playbooks/roles/2fa/files/first-factor @@ -0,0 +1,6 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 8 -*- + +auth requisite pam_faillock.so preauth +auth [success=2 default=ignore] pam_unix.so try_first_pass nullok +-auth [success=1 default=ignore] pam_systemd_home.so +auth [default=die] pam_faillock.so authfail diff --git a/playbooks/roles/2fa/files/remote-switch.access.conf b/playbooks/roles/2fa/files/remote-switch.access.conf new file mode 100644 index 0000000..3351d2d --- /dev/null +++ b/playbooks/roles/2fa/files/remote-switch.access.conf @@ -0,0 +1,2 @@ ++:ALL:LOCAL +-:ALL:ALL diff --git a/playbooks/roles/2fa/files/system-auth b/playbooks/roles/2fa/files/system-auth new file mode 100644 index 0000000..a4622b1 --- /dev/null +++ b/playbooks/roles/2fa/files/system-auth @@ -0,0 +1,6 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +auth include common-auth +account include common-account +password include common-password +session include common-session diff --git a/playbooks/roles/2fa/tasks/main.yml b/playbooks/roles/2fa/tasks/main.yml new file mode 100644 index 0000000..783b6e5 --- /dev/null +++ b/playbooks/roles/2fa/tasks/main.yml @@ -0,0 +1,74 @@ +--- + +- name: 'Install TOTP authenticator and pam_u2f' + ansible.builtin.apt: + name: + - libpam-google-authenticator + - libpam-u2f + - pamu2fcfg + state: present + when: ansible_pkg_mgr == "apt" + +- name: 'Install TOTP authenticator' + community.general.pacman: + name: + - libpam-google-authenticator + - pam-u2f + state: present + when: ansible_pkg_mgr == "pacman" + +- name: 'Deploy PAM remote-user allowlist' + ansible.builtin.copy: + src: remote-switch.access.conf + dest: /etc/security/remote-switch.access.conf + owner: root + group: root + mode: "0644" + +- name: 'Deploy local-users first auth factor' + ansible.builtin.copy: + src: first-factor + dest: /etc/pam.d/first-factor + owner: root + group: root + mode: "0644" + # Only deploy when we're not using Kanidm for native or ldap + when: user_source == "local" + +- name: 'Deploy local-access second auth factor' + ansible.builtin.template: + src: second-factor.j2 + dest: /etc/pam.d/second-factor + owner: root + group: root + mode: "0644" + +- name: 'Deploy PAM common-auth file' + ansible.builtin.copy: + src: common-auth + dest: /etc/pam.d/ + owner: root + group: root + mode: "0644" + +- name: 'Deploy PAM system-auth file' + ansible.builtin.copy: + src: system-auth + dest: /etc/pam.d/ + owner: root + group: root + mode: "0644" + when: ansible_os_family == "Archlinux" + +- name: 'Deploy local-users common PAM files' + ansible.builtin.copy: + src: 'common-{{ item }}' + dest: '/etc/pam.d/' + owner: root + group: root + mode: "0644" + when: user_source == "local" + with_items: + - password + - session + - account diff --git a/playbooks/roles/2fa/templates/second-factor.j2 b/playbooks/roles/2fa/templates/second-factor.j2 new file mode 100644 index 0000000..c0b811d --- /dev/null +++ b/playbooks/roles/2fa/templates/second-factor.j2 @@ -0,0 +1,11 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 8 -*- + +# Only prompt for security key if this is a local session. +auth [success=ignore default=2] pam_access.so accessfile=/etc/security/remote-switch.access.conf +auth [success=2 default=ignore] pam_u2f.so cue origin=pam://{{ ansible_nodename }} appid=pam://{{ ansible_nodename }} userpresence=1 +# This is a moderate security risk due to the nullok, but the alternative is locking ourselves out of remote machines. +# This turns 2FA into an opt-in system. +auth [success=1 default=ignore] pam_google_authenticator.so nullok +# We could change this to 'pam_faillock.so authfail', but idk that that's worth it. +auth [default=die] pam_deny.so +auth optional pam_permit.so diff --git a/playbooks/roles/aur/tasks/main.yml b/playbooks/roles/aur/tasks/main.yml new file mode 100644 index 0000000..c2b79e0 --- /dev/null +++ b/playbooks/roles/aur/tasks/main.yml @@ -0,0 +1,42 @@ +--- + +- name: Install devel packages + community.general.pacman: + name: + - 'base-devel' + - 'git' + when: ansible_pkg_mgr == "pacman" + +- name: Disable package compression + ansible.builtin.replace: + path: '/etc/makepkg.conf' + regexp: "^PKGEXT=.*$" + replace: "PKGEXT='.pkg.tar'" + +- name: Create AUR build user + ansible.builtin.user: + name: aur_builder + system: yes + home: /var/lib/pacman/aur/ + password_lock: yes + shell: /usr/bin/false + when: ansible_pkg_mgr == "pacman" + +- name: Allow AUR build user to run Pacman as root + ansible.builtin.lineinfile: + path: /etc/sudoers.d/aur_builder-allow-to-sudo-pacman + state: present + line: "aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman" + validate: /usr/sbin/visudo -cf %s + create: yes + when: ansible_pkg_mgr == "pacman" + +- name: Install AUR helper using makepkg + kewlfft.aur.aur: + name: paru-bin + use: makepkg + state: present + become: yes + become_user: aur_builder + when: ansible_pkg_mgr == "pacman" + diff --git a/playbooks/roles/kanidm_native/files/common-account b/playbooks/roles/kanidm_native/files/common-account new file mode 100644 index 0000000..8818278 --- /dev/null +++ b/playbooks/roles/kanidm_native/files/common-account @@ -0,0 +1,12 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +# Local users don't authenticate with Kanidm +account [success=1 default=ignore] pam_localuser.so +# When Kanidm fails, jump straight to the deny line. We already know we're not a local user, so this is fine. +account [success=3 default=2] pam_kanidm.so +-account [success=2 default=ignore] pam_systemd_home.so +account [success=1 default=ignore] pam_unix.so +# If any of the above account lines fail, they'll jump here, which kills the authorization attempt. +account [default=die] pam_deny.so +account optional pam_permit.so +account required pam_time.so diff --git a/playbooks/roles/kanidm_native/files/common-password b/playbooks/roles/kanidm_native/files/common-password new file mode 100644 index 0000000..36d3723 --- /dev/null +++ b/playbooks/roles/kanidm_native/files/common-password @@ -0,0 +1,8 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +password [success=1 default=ignore] pam_localuser.so +password [success=3 default=2] pam_kanidm.so +-password [success=2 default=ignore] pam_systemd_home.so +password [success=1 default=ignore] pam_unix.so try_first_pass nullok shadow sha512 +password [default=die] pam_deny.so +password optional pam_permit.so diff --git a/playbooks/roles/kanidm_native/files/common-session b/playbooks/roles/kanidm_native/files/common-session new file mode 100644 index 0000000..30cd7e6 --- /dev/null +++ b/playbooks/roles/kanidm_native/files/common-session @@ -0,0 +1,7 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 10 -*- + +session required pam_limits.so +session optional pam_unix.so +session optional pam_umask.so +session optional pam_kanidm.so +session optional pam_env.so diff --git a/playbooks/roles/kanidm_native/files/first-factor b/playbooks/roles/kanidm_native/files/first-factor new file mode 100644 index 0000000..df85521 --- /dev/null +++ b/playbooks/roles/kanidm_native/files/first-factor @@ -0,0 +1,11 @@ +#%PAM-1.0 -*- mode: conf-space; tab-width: 8 -*- + +# First authentication factor for Kanidm-native systems + +auth requisite pam_faillock.so preauth +auth [success=1 default=ignore] pam_localuser.so +auth [success=3 default=2] pam_kanidm.so +auth [sucesss=2 default=ignore] pam_unix.so try_first_pass nullok +-auth [success=1 default=ignore] pam_systemd_home.so +auth [default=die] pam_faillock.so authfail +auth optional pam_permit.so diff --git a/playbooks/roles/kanidm_native/files/kanidm-hack.sh b/playbooks/roles/kanidm_native/files/kanidm-hack.sh new file mode 100644 index 0000000..a0e7fce --- /dev/null +++ b/playbooks/roles/kanidm_native/files/kanidm-hack.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script is called by systemd-sleep, and is used to prevent the +# daemon from getting stuck when resuming from sleep. +# Only noticed so far on resume from hibernate, but it's possible that +# it could happen on suspend as well. + +if [[ $1 == "post" ]]; then + systemctl restart kanidm-unixd.service +fi diff --git a/playbooks/roles/kanidm_native/handlers/main.yml b/playbooks/roles/kanidm_native/handlers/main.yml new file mode 100644 index 0000000..c03af99 --- /dev/null +++ b/playbooks/roles/kanidm_native/handlers/main.yml @@ -0,0 +1,6 @@ +--- + +- name: Restart SSH + ansible.builtin.systemd: + unit: "sshd.service" + state: "restarted" diff --git a/playbooks/roles/kanidm_native/tasks/main.yml b/playbooks/roles/kanidm_native/tasks/main.yml new file mode 100644 index 0000000..c93d2ba --- /dev/null +++ b/playbooks/roles/kanidm_native/tasks/main.yml @@ -0,0 +1,109 @@ +--- + +- name: 'Install kanidm clients with pacman' + kewlfft.aur.aur: + use: paru + aur_only: yes + name: kanidm-unixd-clients + become: yes + become_user: aur_builder + when: ansible_pkg_mgr == "pacman" + +- name: 'Fetch kanidm PPA key' + ansible.builtin.apt_key: + url: >- + https://kanidm.github.io/kanidm_ppa/KEY.gpg + state: present + id: 'EA20E95D68A65191FE8CE79576CC814060B23E66' + when: ansible_pkg_mgr == "apt" + +- name: 'Create kanidm PPA' + ansible.builtin.apt_repository: + repo: >- + deb https://kanidm.github.io/kanidm_ppa/{{ ansible_distribution | lower }} ./ + state: present + when: ansible_pkg_mgr == "apt" + +- name: 'Install kanidm with apt' + ansible.builtin.apt: + name: kanidm-unixd-clients + state: present + update_cache: yes + when: ansible_pkg_mgr == "apt" + +- name: 'Ensure kanidm config directory exists' + ansible.builtin.file: + path: /etc/kanidm + state: directory + owner: root + group: root + mode: "0755" + +- name: 'Install kanidm config files' + ansible.builtin.template: + src: '{{ item }}.j2' + dest: '/etc/kanidm/{{ item }}' + owner: root + group: root + mode: "0644" + with_items: + - unixd + - config + +- name: 'Enable kanidm daemons' + ansible.builtin.systemd_service: + state: started + enabled: yes + name: "{{ item }}" + daemon_reload: yes + with_items: + - kanidm-unixd + - kanidm-unixd-tasks + + +- name: 'Enable kanidm as a passwd db' + ansible.builtin.replace: + path: '/etc/nsswitch.conf' + regexp: "^{{ item }}:.*$" + replace: "{{ item }}: files {{ (item == 'group') | ternary('[SUCCESS=merge]', '') }} systemd compat kanidm" + # This is a critical system file that could brick the OS. Back it up! + backup: yes + with_items: + - passwd + - group + +- name: 'Deploy first-factor PAM configuration' + ansible.builtin.copy: + src: first-factor + dest: /etc/pam.d/first-factor + owner: root + group: root + mode: "0644" + +- name: 'Deploy common PAM modules for kanidm' + ansible.builtin.copy: + src: '{{ item }}' + dest: /etc/pam.d/ + owner: root + group: root + mode: "0644" + with_fileglob: + - "../files/common-*" + +- name: 'Deploy SSH key handling' + ansible.builtin.template: + src: 10-kanidm-keys.conf.j2 + dest: /etc/ssh/sshd_config.d/ + owner: root + group: root + mode: "0644" + notify: Restart SSH + +- name: 'Deploy sleep fix hack' + ansible.builtin.copy: + src: kanidm-hack.sh + dest: /usr/lib/systemd/system-sleep/ + owner: root + group: root + mode: "0755" + diff --git a/playbooks/roles/kanidm_native/templates/10-kanidm-keys.conf.j2 b/playbooks/roles/kanidm_native/templates/10-kanidm-keys.conf.j2 new file mode 100644 index 0000000..7cf2a9a --- /dev/null +++ b/playbooks/roles/kanidm_native/templates/10-kanidm-keys.conf.j2 @@ -0,0 +1,6 @@ +PubkeyAuthentication yes +UsePAM yes + +Match Group {{ allowed_groups | join(',') }} + AuthorizedKeysCommand /usr/sbin/kanidm_ssh_authorizedkeys %u + AuthorizedKeysCommandUser nobody diff --git a/playbooks/roles/kanidm_native/templates/config.j2 b/playbooks/roles/kanidm_native/templates/config.j2 new file mode 100644 index 0000000..089191f --- /dev/null +++ b/playbooks/roles/kanidm_native/templates/config.j2 @@ -0,0 +1,8 @@ +uri = "{{ kanidm_uri }}" +verify_ca = true +verify_hostnames = true + +{% for name, uri in kanidm_supplemental %} +[{{ name }}] +uri = "{{ uri }}" +{% endfor %} diff --git a/playbooks/roles/kanidm_native/templates/unixd.j2 b/playbooks/roles/kanidm_native/templates/unixd.j2 new file mode 100644 index 0000000..5091cff --- /dev/null +++ b/playbooks/roles/kanidm_native/templates/unixd.j2 @@ -0,0 +1,11 @@ +# -*-conf-unix-*- + +pam_allowed_login_groups = {{ allowed_groups }} +default_shell = "/bin/bash" +home_attr = "uuid" +home_alias = "name" +use_etc_skel = true +# Just use name for usernames, it shouldn't be too hard to keep track of +uid_attr_map = "name" +# Use the SPN for groups, so we can easily tell which are local and which are remote +gid_attr_map = "spn" diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..8b7a67c --- /dev/null +++ b/requirements.yml @@ -0,0 +1,6 @@ +--- +collections: + - name: >- + kewlfft.aur + - name: >- + community.general