aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHalfwalker <deano-gitea@areyes.com>2024-12-13 16:27:12 -0700
committerNicholas Johnson <mail@nicholasjohnson.ch>2025-01-29 00:00:00 +0000
commit86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266 (patch)
tree190103ca60d3b4a63c06fa777ad823e426b69186201bbedc60909161b4946c09
downloadansible-role-google-authenticator-86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266.tar.gz
ansible-role-google-authenticator-86bd5e0eccec2453afd1ab11a1221d6e3f0e819ba8386b3edf50387f69ff9266.zip
Initial commit
-rw-r--r--.ansible-lint16
-rw-r--r--.gitignore26
-rw-r--r--.yamllint37
-rw-r--r--README.md85
-rw-r--r--defaults/main.yml42
-rw-r--r--tasks/main.yml178
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
+
+![Build Status](http://bondi.local:3001/api/badges/Halfwalker/ansible-googleauth/status.svg)
+
+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
+
+