diff options
author | Halfwalker <deano-gitea@areyes.com> | 2024-12-13 16:27:12 -0700 |
---|---|---|
committer | Nicholas Johnson <mail@nicholasjohnson.ch> | 2025-01-29 00:00:00 +0000 |
commit | 86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266 (patch) | |
tree | 190103ca60d3b4a63c06fa777ad823e426b69186201bbedc60909161b4946c09 | |
download | ansible-role-google-authenticator-86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266.tar.gz ansible-role-google-authenticator-86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266.zip |
Initial commit
-rw-r--r-- | .ansible-lint | 16 | ||||
-rw-r--r-- | .gitignore | 26 | ||||
-rw-r--r-- | .yamllint | 37 | ||||
-rw-r--r-- | README.md | 85 | ||||
-rw-r--r-- | defaults/main.yml | 42 | ||||
-rw-r--r-- | tasks/main.yml | 178 |
6 files changed, 384 insertions, 0 deletions
diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..de68378 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,16 @@ +# .ansible-lint + +skip_list: + - risky-file-permissions + - no-changed-when + - command-instead-of-module + - unnamed-task + - git-latest + - fqcn[action-core] # Use FQCN for builtin actions + - fqcn[action] # Use FQCN for builtin actions +warn_list: + - package-latest + - ignore-errors + - jinja[spacing] # Rule that looks inside jinja2 templates + - experimental # all rules tagged as experimental + - name[casing] # Rule for checking task and play names diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23480a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# ---> Ansible +*.retry + +# ---> Vim +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# For running drone locally +drone_secrets.txt diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..65542e3 --- /dev/null +++ b/.yamllint @@ -0,0 +1,37 @@ +--- +# Based on ansible-lint config +extends: default + +rules: + braces: + max-spaces-inside: 1 + level: error + brackets: + max-spaces-inside: 1 + level: error + colons: + max-spaces-after: -1 + level: error + commas: + max-spaces-after: -1 + level: error + comments: + min-spaces-from-content: 1 + comments-indentation: disable + octal-values: + forbid-implicit-octal: true # yamllint defaults to false + document-start: disable + empty-lines: + max: 3 + max-end: 2 + level: warning + hyphens: + level: error + indentation: disable + key-duplicates: enable + line-length: disable + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable + truthy: disable diff --git a/README.md b/README.md new file mode 100644 index 0000000..3379199 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# ansible-googleauth + + + +This role is to install google authenticator and integrate it into ssh so that TOTP tokens may be required for ssh connections. + +It will create a `~/.google_authenticator` if required, and will NOT alter or remove any existing version. + +It will update `/etc/ssh/sshd_config.d` to ensure that a token is required for any ssh connection without an ssh key. Connections _with_ an ssh key will not require a token, though this may be enabled so that tokens are *always* required. Set the global **google_auth_force** variable to _true_ or an individual host entry (see below) to enable this. + +## Configuration + +To pre-populate the TOTP secret there are two locations to place the information. + +* Place them into `defaults/main.yml` under the **google_auth_config** variable +* *Much* more preferably place them into an ansible-vault encrypted file under the **vault_google_auth_config** variable. Typically this might be in `group_vars/all/vault` + +The format is as follows +| Variable | Description | +| :--- | :--- | +| name: | The inventory_hostname for this block | +| force_auth: | force token for ALL ssh connections for this host | +| secret: | Standard `.google_authenticator` secret info + +```yaml +# 1st line of secret can be 16 or 26 chars +vault_google_auth_config: + - name: host1.example.com + force_auth: false + secret: | + 6DRWZ2AWOAFAQMSI + "RATE_LIMIT 3 30 + " WINDOW_SIZE 3 + " DISALLOW_REUSE + " TOTP_AUTH + 36011504 + 52878834 + 36710801 + 23387673 + 16670568 + - name: hosty.somewhere.com + force_auth: false + secret: | + MVXECANUVTIQ2647HK3S35FM3A + " RATE_LIMIT 3 30 1734051365 + " DISALLOW_REUSE 57801712 + " TOTP_AUTH + 17029728 + 27355189 + 27432004 + 50794981 + 18624382 +``` + +NOTE: There must be at least 5x scratch codes for each secret + +## Manually generating a google authenticator secret + +For pre-populating the host secrets in the config, one can generate them via the command-line. For an Ubuntu system, ensure the following packages are installed : + +* libpam-google-authenticator +* python3-qrcode +* qrencode + +Generate an authenticator secret, placed in `/tmp/google.txt` + +```bash +google-authenticator --time-based --disallow-reuse --label=Test1 --qr-mode=UTF8 --rate-limit=3 --rate-time=30 --secret=/tmp/google.txt --window-size=3 --force +``` + +The contents of the resulting `/tmp/google.txt` may be placed directly into the `vault_google_auth_config` variable for a specific host. + +## Methods of installation + +There are several methods of creating the `~/.google_authenticator` file + +* Existing `~/.google_authenticator`, no pre-config + Any existing configuration will not be touched +* Existing `~/.google_authenticator`, pre-config in **vault_google_auth_config** + Any existing configuration will not be touched. You must manually remove any existing `~/.google_authenticator` +* No existing `~/.google_authenticator`, no pre-config + In this case a new secret key and scratch codes will be created +* No existing`~/.google_authenticator`, pre-config in **vault_google_auth_config** + If an entry in **vault_google_auth_config** exists it will be used, otherwise a new secret key will be created + diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..7545e05 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,42 @@ +--- + +# User can be overridden by a vault variable or from hosts file entry or ansible cmdline +username: "{{ vault_username | default(ansible_user_id) }}" + +# Use google authenticator config from vault if it's there +# 1st line secret can be 16 or 26 chars +# vault_google_auth_config: +# - name: host1.example.com +# force_auth: false +# secret: | +# 6DRWZ2AWOAFAQMSI +# "RATE_LIMIT 3 30 +# " WINDOW_SIZE 3 +# " DISALLOW_REUSE +# " TOTP_AUTH +# 36011504 +# 52878834 +# 36710801 +# 23387673 +# 16670568 +# - name: hosty.somewhere.com +# force_auth: false +# secret: | +# MVXECANUVTIQ2647HK3S35FM3A +# " RATE_LIMIT 3 30 1734051365 +# " DISALLOW_REUSE 57801712 +# " TOTP_AUTH +# 17029728 +# 27355189 +# 27432004 +# 50794981 +# 18624382 +google_auth_config: "{{ vault_google_auth_config | default('NEW') }}" + +# Force use of token even with SSH key +google_auth_force: false + +google_auth_packages: + - libpam-google-authenticator + - python3-qrcode + - qrencode diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..3ccda8e --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,178 @@ +--- + +# ✅ Existing local file vault secret +# ✅ Existing local file no vault +# ✅ No local file vault secret +# ✅ No local file no vault + +# Sets up google authenticator and integrates with login + +# Ensure we have the user's group +- when: groupname is not defined + block: + - name: Get passwd DB entry for {{ username }} + ansible.builtin.getent: + database: passwd + key: "{{ username }}" + register: user_pw + + - name: Get group DB entry for {{ username }} + ansible.builtin.getent: + database: group + key: "{{ user_pw.ansible_facts.getent_passwd[username][2] }}" + register: user_gid + + - name: Set groupname fact to {{ user_gid.ansible_facts.getent_group.keys()|first }} + set_fact: + groupname: "{{ user_gid.ansible_facts.getent_group.keys()|first }}" + # + # block + +# Install packages +- name: Install google authenticator packages + ansible.builtin.apt: + state: present + update_cache: true + install_recommends: no + name: "{{ google_auth_packages }}" + +# +# If google_auth_config is defined then use those values to build .google_authenticator etc. +# + +- name: Check for existing /home/{{ username }}/.google_authenticator config + ansible.builtin.stat: + path: "/home/{{ username }}/.google_authenticator" + register: google_auth_config_local + +# Only generate a new config if no existing local one +- when: not google_auth_config_local.stat.exists # No existing secret + block: + + # Create new .google_authenticator from vault config if vault defined + - name: Create .google_authenticator file + ansible.builtin.copy: + content: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='secret') | first }}" + dest: "/home/{{ username }}/.google_authenticator" + mode: '0400' + owner: "{{ username }}" + group: "{{ username }}" + when: + - google_auth_config != "NEW" # vault_google_auth_config exists + - google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 # vault entry for THIS host exists + + # If no vault config defined, or no entry defined for this hostname + - when: google_auth_config == "NEW" or google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length == 0 + block: + # Generate secret + - name: Generate secret for google authenticator + ansible.builtin.command: "/usr/bin/google-authenticator --time-based --disallow-reuse --label=email_{{ inventory_hostname }} --qr-mode=UTF8 --rate-limit=3 --rate-time=30 --secret=/home/{{ username }}/.google_authenticator --window-size=3 --force --quiet" + args: + creates: "/home/{{ username }}/.google_authenticator" + register: google_auth_create + + # Set .google_authenticator to mode 400 + - name: Set new secret file permissions + ansible.builtin.file: + path: "/home/{{ username }}/.google_authenticator" + owner: "{{ username }}" + group: "{{ groupname }}" + mode: '0400' + # + # block no vault config or no entry defined + # + # block not local config exists + + +# +# Now we deal with a .google_authernticator, regardless of whether it already existed +# or was newly created, or was created from a vault config +# +- name: Pulling in /home/{{ username }}/.google_authenticator + ansible.builtin.slurp: + src: "/home/{{ username }}/.google_authenticator" + register: google_auth_file + +- name: Set google auth config fact + ansible.builtin.set_fact: + google_auth_config_mine: "{{ google_auth_file['content'] | b64decode }}" + +- name: Parse TOTP variable + ansible.builtin.set_fact: + totp_lines: "{{ google_auth_config_mine.split('\n') | map('trim') | list }}" + +- name: Filter valid lines + ansible.builtin.set_fact: + valid_lines: "{{ totp_lines | reject('search', '^\"') | list }}" + +# Main secret must be 16 or 26 chars. Must be at least 5x scratch codes +- name: Validate secret and scratch codes + ansible.builtin.assert: + that: + - "valid_lines[0] is defined and valid_lines[0] | length in [16, 26] and valid_lines[0] is match('^[A-Z0-9]+$')" + - "valid_lines[1:] | select('match', '^[0-9]{8}$') | list | length >= 5" + + fail_msg: "The TOTP variable does not meet the required structure." + success_msg: "The TOTP variable is valid." + +- debug: + var: google_auth_config_mine + +# Capture secret key - GOOGLE_SECRET=$(head -1 .google_authenticator +- name: Extract Google Authenticator secret key + ansible.builtin.set_fact: + google_secret_key: "{{ valid_lines[0] }}" + +# Capture scratch codes - this creates a var like +# google_scratch_codes: +# - 21528074 +# - 86134509 +# - 79251446 +- name: Extract Google Authenticator scratch codes + ansible.builtin.set_fact: + google_scratch_codes: "{{ valid_lines | select('match', '^[0-9]{8}$') | list }}" + +- debug: + var: google_secret_key +- debug: + var: google_scratch_codes + +# Create QR code +- name: Create QR code for secret + ansible.builtin.command: "/usr/bin/qrencode -m 3 -t UTF8 otpauth://totp/{{ inventory_hostname }}:{{ username }}%3Fsecret={{ google_secret_key }}%3FIssuer={{ inventory_hostname_short }}_mailsys" + register: google_auth_qrcode + +- debug: + msg: "{{ google_auth_qrcode.stdout }}" + + +# Set pam to use google authenticator for ssh +# echo "auth required pam_google_authenticator.so" >> /etc/pam.d/sshd +- name: Set pam to use google authenticator for ssh + ansible.builtin.lineinfile: + path: /etc/pam.d/sshd + insertafter: EOF + line: 'auth required pam_google_authenticator.so' + state: present + +- name: Modify sshd_config to use google authenticator + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/70-google_auth.conf + content: | + # + # For google authenticator + # + ChallengeResponseAuthentication yes + +- name: Modify sshd_config to force use of google authenticator + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/71-google_auth.conf + content: | + # + # For google authenticator to force use of token always + # + PasswordAuthentication no + # Only when global google_auth_force is true OR specific inventory_hostname has force_auth: true + when: google_auth_force == true or google_auth_config | selectattr('name', 'equalto', inventory_hostname) | selectattr('force_auth', 'equalto', true) | list | length > 0 + + |