diff options
Diffstat (limited to 'ansible/roles/certs')
-rwxr-xr-x | ansible/roles/certs/files/leforward.py | 68 | ||||
-rw-r--r-- | ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem | 27 | ||||
-rw-r--r-- | ansible/roles/certs/tasks/main.yml | 153 |
3 files changed, 248 insertions, 0 deletions
diff --git a/ansible/roles/certs/files/leforward.py b/ansible/roles/certs/files/leforward.py new file mode 100755 index 0000000..dccbac1 --- /dev/null +++ b/ansible/roles/certs/files/leforward.py | |||
@@ -0,0 +1,68 @@ | |||
1 | #!/usr/bin/env python3 | ||
2 | |||
3 | """ Run a single-purpose HTTP server. | ||
4 | |||
5 | Server takes all GET requests and redirects them to a new host | ||
6 | if the request URI starts with SUBPATH, otherwise returns 404. | ||
7 | |||
8 | Requests are redirected to the URL provided by --baseurl. """ | ||
9 | |||
10 | import socketserver | ||
11 | import http.server | ||
12 | import argparse | ||
13 | import sys | ||
14 | |||
15 | |||
16 | CHALLENGE_HOST = None | ||
17 | SUBPATH = "/.well-known/acme-challenge" | ||
18 | |||
19 | |||
20 | class RedirectChallenges(http.server.BaseHTTPRequestHandler): | ||
21 | def do_GET(self): | ||
22 | if self.path.startswith(SUBPATH): | ||
23 | self.send_response(301) | ||
24 | self.send_header('Location', f"{CHALLENGE_HOST}{self.path}") | ||
25 | else: | ||
26 | self.send_response(404) | ||
27 | |||
28 | self.end_headers() | ||
29 | |||
30 | |||
31 | class ReusableServer(socketserver.TCPServer): | ||
32 | """ Allow TCPServer to reuse host address. | ||
33 | |||
34 | Without setting 'allow_reuse_address', we can get stuck in | ||
35 | TIME_WAIT after being killed and the stale state stops a new | ||
36 | server from attaching to the port.""" | ||
37 | |||
38 | allow_reuse_address = True | ||
39 | |||
40 | |||
41 | if __name__ == "__main__": | ||
42 | parser = argparse.ArgumentParser( | ||
43 | description="Redirect all URIs with matching prefix to another host") | ||
44 | parser.add_argument( | ||
45 | '--baseurl', | ||
46 | dest='baseurl', | ||
47 | required=True, | ||
48 | help="Destination URL for all matching URIs on this server") | ||
49 | |||
50 | args = parser.parse_args() | ||
51 | CHALLENGE_HOST = args.baseurl | ||
52 | |||
53 | if not CHALLENGE_HOST.startswith("http"): | ||
54 | print("Redirect URL must be a full URL starting with http") | ||
55 | sys.exit(1) | ||
56 | |||
57 | # If user gave us a trailing slash URL, remove slash. | ||
58 | if CHALLENGE_HOST[-1] == "/": | ||
59 | CHALLENGE_HOST = CHALLENGE_HOST[:-1] | ||
60 | |||
61 | serverAddress = ('', 80) | ||
62 | |||
63 | # Note: if running remotely by an SSH command, you MUST launch with '-t': | ||
64 | # > ssh -t me@otherhost leforward.py --baseurl http://otherserver.com | ||
65 | # If you omit '-t' the listening server won't terminate when you kill the | ||
66 | # ssh session, which probably isn't what you want. | ||
67 | with ReusableServer(serverAddress, RedirectChallenges) as httpd: | ||
68 | httpd.serve_forever() | ||
diff --git a/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem b/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem new file mode 100644 index 0000000..0002462 --- /dev/null +++ b/ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem | |||
@@ -0,0 +1,27 @@ | |||
1 | -----BEGIN CERTIFICATE----- | ||
2 | MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ | ||
3 | MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT | ||
4 | DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow | ||
5 | SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT | ||
6 | GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC | ||
7 | AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF | ||
8 | q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 | ||
9 | SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 | ||
10 | Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA | ||
11 | a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj | ||
12 | /PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T | ||
13 | AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG | ||
14 | CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv | ||
15 | bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k | ||
16 | c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw | ||
17 | VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC | ||
18 | ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz | ||
19 | MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu | ||
20 | Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF | ||
21 | AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo | ||
22 | uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ | ||
23 | wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu | ||
24 | X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG | ||
25 | PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 | ||
26 | KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== | ||
27 | -----END CERTIFICATE----- | ||
diff --git a/ansible/roles/certs/tasks/main.yml b/ansible/roles/certs/tasks/main.yml new file mode 100644 index 0000000..e83a640 --- /dev/null +++ b/ansible/roles/certs/tasks/main.yml | |||
@@ -0,0 +1,153 @@ | |||
1 | --- | ||
2 | - name: remove default ubuntu key | ||
3 | file: | ||
4 | path: /etc/ssl/private/ssl-cert-snakeoil.key | ||
5 | state: absent | ||
6 | |||
7 | - name: create cert maint group | ||
8 | group: | ||
9 | name: certmaint | ||
10 | gid: 1070 | ||
11 | state: present | ||
12 | |||
13 | - name: create cert maint user | ||
14 | user: | ||
15 | name: certmaint | ||
16 | uid: 1070 | ||
17 | group: ssl-cert | ||
18 | groups: | ||
19 | - certmaint | ||
20 | shell: /bin/sh | ||
21 | create_home: yes | ||
22 | state: present | ||
23 | |||
24 | #- name: allow certmaint to maint certs and keys (default) | ||
25 | # acl: | ||
26 | # path: /etc/ssl/ | ||
27 | # etype: user | ||
28 | # entity: certmaint | ||
29 | # permissions: rw | ||
30 | # default: yes | ||
31 | # recursive: yes | ||
32 | # state: present | ||
33 | # no_log: true | ||
34 | |||
35 | #- name: allow certmaint to maint certs and keys (actual certs) | ||
36 | # acl: | ||
37 | # path: /etc/ssl/ | ||
38 | # etype: user | ||
39 | # entity: certmaint | ||
40 | # permissions: rwx | ||
41 | # state: present | ||
42 | # no_log: true | ||
43 | |||
44 | #- name: allow certmaint to maint certs and keys (actual keys) | ||
45 | # acl: | ||
46 | # path: /etc/ssl/private/ | ||
47 | # etype: user | ||
48 | # entity: certmaint | ||
49 | # permissions: rwx | ||
50 | # state: present | ||
51 | # no_log: true | ||
52 | |||
53 | # Keys are private: only owner can read/write, and only group can read | ||
54 | - name: populate required keys (common types) | ||
55 | copy: | ||
56 | src: "tls/private/{{ item[0] }}-key.{{ item[1] }}.pem" | ||
57 | dest: /etc/ssl/private/ | ||
58 | mode: 0640 | ||
59 | owner: certmaint | ||
60 | group: ssl-cert | ||
61 | loop: "{{ certs.required |product(certs.keyTypes) |list }}" | ||
62 | when: certs.required[0] is string | ||
63 | |||
64 | |||
65 | # Certs are owned by 'certmaint' so user 'certmaint' can update them over scp | ||
66 | # Certs are public (obviously) | ||
67 | - name: populate required certs (common types) | ||
68 | copy: | ||
69 | src: "tls/{{ item[0] }}-cert-combined.{{ item[1] }}.pem" | ||
70 | dest: /etc/ssl/ | ||
71 | mode: 0644 | ||
72 | owner: certmaint | ||
73 | loop: "{{ certs.required |product(certs.keyTypes) |list }}" | ||
74 | when: certs.required[0] is string | ||
75 | |||
76 | |||
77 | |||
78 | # Keys are private: only owner can read/write, and only group can read | ||
79 | - name: populate required keys (specific types) | ||
80 | copy: | ||
81 | src: "tls/private/{{ item.host }}-key.{{ item.type }}.pem" | ||
82 | dest: /etc/ssl/private/ | ||
83 | mode: 0640 | ||
84 | owner: certmaint | ||
85 | group: ssl-cert | ||
86 | loop: "{{ certs.required }}" | ||
87 | when: certs.required[0] is mapping | ||
88 | |||
89 | # Certs are owned by 'certmaint' so user 'certmaint' can update them over scp | ||
90 | # Certs are public (obviously) | ||
91 | - name: populate required certs (specific types) | ||
92 | copy: | ||
93 | src: "tls/{{ item.host }}-cert-combined.{{ item.type }}.pem" | ||
94 | dest: /etc/ssl/ | ||
95 | mode: 0644 | ||
96 | owner: certmaint | ||
97 | loop: "{{ certs.required }}" | ||
98 | when: certs.required[0] is mapping | ||
99 | |||
100 | |||
101 | |||
102 | - name: plop LE cert chain | ||
103 | copy: | ||
104 | src: "tls/lets-encrypt-x3-cross-signed.pem" | ||
105 | dest: /etc/ssl/ | ||
106 | mode: 0644 | ||
107 | owner: certmaint | ||
108 | |||
109 | - name: plop remote LE challenge redirector | ||
110 | copy: | ||
111 | src: leforward.py | ||
112 | dest: /usr/local/bin/ | ||
113 | mode: 0755 | ||
114 | when: | ||
115 | - certs.receiver is defined and certs.receiver | ||
116 | |||
117 | |||
118 | # Retrieve all users on this host (creates variable 'passwd' containing results) | ||
119 | - name: get all user details so we can populate home directories | ||
120 | getent: | ||
121 | database: passwd | ||
122 | |||
123 | # Copy users/hostname/username contents into remote home directory | ||
124 | - name: verify explicit user keys exist as expected | ||
125 | copy: | ||
126 | src: "users/{{ inventory_hostname }}/{{ item }}/" | ||
127 | # [item][4] is [username][homedir] where /etc/passwd is tokenized on ':' | ||
128 | # and username becomes the key with remaining fields indexed by integers | ||
129 | dest: "{{ getent_passwd[item][4] }}" | ||
130 | mode: 0600 | ||
131 | owner: "{{ item }}" | ||
132 | directory_mode: 0700 | ||
133 | loop: "{{ certs.sshKeysForUsers }}" | ||
134 | |||
135 | # TODO: we could make one key per action then restrict actions by ssh key. | ||
136 | # (postfix key, dovecot key, nginx key, leforward key) | ||
137 | - name: verify certmaint receiver key exists | ||
138 | copy: | ||
139 | src: "users/certmaint/" | ||
140 | dest: "{{ getent_passwd[item][4] }}" | ||
141 | mode: 0600 | ||
142 | owner: "{{ item }}" | ||
143 | directory_mode: 0700 | ||
144 | loop: | ||
145 | - certmaint | ||
146 | |||
147 | - name: allow certmaint group to sudo reload relevant services | ||
148 | lineinfile: | ||
149 | path: /etc/sudoers.d/certmaint_reloads | ||
150 | regexp: "^%certmaint" | ||
151 | line: "%certmaint ALL = (root) NOPASSWD: /usr/sbin/service postfix reload, /usr/sbin/service dovecot reload, /usr/sbin/service nginx reload" | ||
152 | create: yes | ||
153 | mode: 0440 | ||