aboutsummaryrefslogtreecommitdiff
path: root/tasks/main.yml
blob: 96c26417d60d43bcd541ac85b58cc2d227e113b3f865594fe8e48e6f5fdd2654 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
---

# ✅  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_authenticator, 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 }}"

# If label or issuer are not set in google_auth_config then the existing default values will be used
- 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
          #
          KbdInteractiveAuthentication yes

    - name: Instruct PAM to prompt for a password by default
      ansible.builtin.replace:
        path: "/etc/pam.d/sshd"
        regexp: '^#.*@include common-auth'
        replace: '@include common-auth'

    - when: google_auth_force == true
      block:
        - 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
              # Only when global google_auth_force is true
              #
              AuthenticationMethods publickey,password publickey,keyboard-interactive

        - name: Instruct PAM to not prompt for a password
          ansible.builtin.replace:
            path: "/etc/pam.d/sshd"
            regexp: '^@include common-auth'
            replace: '# @include common-auth'
      #
      # block google_auth_force
  #
  # block system file updates