--- # ✅ 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 }}" become: yes # # 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." # 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 }}" - name: Extract label for {{ inventory_hostname }} if it exists ansible.builtin.set_fact: google_auth_label: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='label') | first }}" when: > google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 and (google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('label') is not none and (google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('label') != '' - name: Extract issuer for {{ inventory_hostname }} if it exists ansible.builtin.set_fact: google_auth_issuer: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='issuer') | first }}" when: > google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 and (google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('issuer') is not none and (google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('issuer') != '' # Create QR code - name: Create QR code for secret ansible.builtin.command: "/usr/bin/qrencode -m 3 -t UTF8 otpauth://totp/{{ google_auth_label }}?secret={{ google_secret_key }}&Issuer={{ google_auth_issuer }}" register: google_auth_qrcode - debug: var: google_secret_key - debug: var: google_scratch_codes - debug: msg: "{{ google_auth_qrcode.stdout }}" - become: yes block: # Set pam to use google authenticator for ssh # echo "auth required pam_google_authenticator.so" >> /etc/pam.d/sshd # echo "auth required pam_google_authenticator.so nullok" >> /etc/pam.d/sshd # The nullok allows regular login if ~/.google_authenticator doesn't exist # Putting at beginning of file means it will ask for token FIRST, then password # This prevents someone from being able to attempt passwords until/unless they have token - name: Set pam to use google authenticator for ssh ansible.builtin.lineinfile: path: /etc/pam.d/sshd insertafter: BOF line: 'auth required pam_google_authenticator.so{% if google_auth_nullok %} nullok{% endif %}' state: present # Must have at least one SUCCESS answer - nullok makes sshd answer IGNORE # Adding pam_permit to end ensure that sshd module will answer SUCCESS if nothing else does # https://github.com/google/google-authenticator-libpam#nullok - name: Set pam to use pam_permit if nullok is defined ansible.builtin.lineinfile: path: /etc/pam.d/sshd insertafter: EOF line: 'auth required pam_permit.so' state: present when: google_auth_nullok - 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) | list | length > 0 and (google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('force_auth') is true) # # block system file updates