From 1204730924436ef9e1c7c49c9557837f9a5ed0e8 Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Wed, 8 Feb 2023 00:40:09 -0800 Subject: fork https://github.com/mattsta/mailweb --- ansible/roles/certs/files/leforward.py | 68 +++++++++ .../certs/files/lets-encrypt-x3-cross-signed.pem | 27 ++++ ansible/roles/certs/tasks/main.yml | 153 +++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100755 ansible/roles/certs/files/leforward.py create mode 100644 ansible/roles/certs/files/lets-encrypt-x3-cross-signed.pem create mode 100644 ansible/roles/certs/tasks/main.yml (limited to 'ansible/roles/certs') 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 @@ +#!/usr/bin/env python3 + +""" Run a single-purpose HTTP server. + +Server takes all GET requests and redirects them to a new host +if the request URI starts with SUBPATH, otherwise returns 404. + +Requests are redirected to the URL provided by --baseurl. """ + +import socketserver +import http.server +import argparse +import sys + + +CHALLENGE_HOST = None +SUBPATH = "/.well-known/acme-challenge" + + +class RedirectChallenges(http.server.BaseHTTPRequestHandler): + def do_GET(self): + if self.path.startswith(SUBPATH): + self.send_response(301) + self.send_header('Location', f"{CHALLENGE_HOST}{self.path}") + else: + self.send_response(404) + + self.end_headers() + + +class ReusableServer(socketserver.TCPServer): + """ Allow TCPServer to reuse host address. + + Without setting 'allow_reuse_address', we can get stuck in + TIME_WAIT after being killed and the stale state stops a new + server from attaching to the port.""" + + allow_reuse_address = True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Redirect all URIs with matching prefix to another host") + parser.add_argument( + '--baseurl', + dest='baseurl', + required=True, + help="Destination URL for all matching URIs on this server") + + args = parser.parse_args() + CHALLENGE_HOST = args.baseurl + + if not CHALLENGE_HOST.startswith("http"): + print("Redirect URL must be a full URL starting with http") + sys.exit(1) + + # If user gave us a trailing slash URL, remove slash. + if CHALLENGE_HOST[-1] == "/": + CHALLENGE_HOST = CHALLENGE_HOST[:-1] + + serverAddress = ('', 80) + + # Note: if running remotely by an SSH command, you MUST launch with '-t': + # > ssh -t me@otherhost leforward.py --baseurl http://otherserver.com + # If you omit '-t' the listening server won't terminate when you kill the + # ssh session, which probably isn't what you want. + with ReusableServer(serverAddress, RedirectChallenges) as httpd: + 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 @@ +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----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 @@ +--- +- name: remove default ubuntu key + file: + path: /etc/ssl/private/ssl-cert-snakeoil.key + state: absent + +- name: create cert maint group + group: + name: certmaint + gid: 1070 + state: present + +- name: create cert maint user + user: + name: certmaint + uid: 1070 + group: ssl-cert + groups: + - certmaint + shell: /bin/sh + create_home: yes + state: present + +#- name: allow certmaint to maint certs and keys (default) +# acl: +# path: /etc/ssl/ +# etype: user +# entity: certmaint +# permissions: rw +# default: yes +# recursive: yes +# state: present +# no_log: true + +#- name: allow certmaint to maint certs and keys (actual certs) +# acl: +# path: /etc/ssl/ +# etype: user +# entity: certmaint +# permissions: rwx +# state: present +# no_log: true + +#- name: allow certmaint to maint certs and keys (actual keys) +# acl: +# path: /etc/ssl/private/ +# etype: user +# entity: certmaint +# permissions: rwx +# state: present +# no_log: true + +# Keys are private: only owner can read/write, and only group can read +- name: populate required keys (common types) + copy: + src: "tls/private/{{ item[0] }}-key.{{ item[1] }}.pem" + dest: /etc/ssl/private/ + mode: 0640 + owner: certmaint + group: ssl-cert + loop: "{{ certs.required |product(certs.keyTypes) |list }}" + when: certs.required[0] is string + + +# Certs are owned by 'certmaint' so user 'certmaint' can update them over scp +# Certs are public (obviously) +- name: populate required certs (common types) + copy: + src: "tls/{{ item[0] }}-cert-combined.{{ item[1] }}.pem" + dest: /etc/ssl/ + mode: 0644 + owner: certmaint + loop: "{{ certs.required |product(certs.keyTypes) |list }}" + when: certs.required[0] is string + + + +# Keys are private: only owner can read/write, and only group can read +- name: populate required keys (specific types) + copy: + src: "tls/private/{{ item.host }}-key.{{ item.type }}.pem" + dest: /etc/ssl/private/ + mode: 0640 + owner: certmaint + group: ssl-cert + loop: "{{ certs.required }}" + when: certs.required[0] is mapping + +# Certs are owned by 'certmaint' so user 'certmaint' can update them over scp +# Certs are public (obviously) +- name: populate required certs (specific types) + copy: + src: "tls/{{ item.host }}-cert-combined.{{ item.type }}.pem" + dest: /etc/ssl/ + mode: 0644 + owner: certmaint + loop: "{{ certs.required }}" + when: certs.required[0] is mapping + + + +- name: plop LE cert chain + copy: + src: "tls/lets-encrypt-x3-cross-signed.pem" + dest: /etc/ssl/ + mode: 0644 + owner: certmaint + +- name: plop remote LE challenge redirector + copy: + src: leforward.py + dest: /usr/local/bin/ + mode: 0755 + when: + - certs.receiver is defined and certs.receiver + + +# Retrieve all users on this host (creates variable 'passwd' containing results) +- name: get all user details so we can populate home directories + getent: + database: passwd + +# Copy users/hostname/username contents into remote home directory +- name: verify explicit user keys exist as expected + copy: + src: "users/{{ inventory_hostname }}/{{ item }}/" + # [item][4] is [username][homedir] where /etc/passwd is tokenized on ':' + # and username becomes the key with remaining fields indexed by integers + dest: "{{ getent_passwd[item][4] }}" + mode: 0600 + owner: "{{ item }}" + directory_mode: 0700 + loop: "{{ certs.sshKeysForUsers }}" + +# TODO: we could make one key per action then restrict actions by ssh key. +# (postfix key, dovecot key, nginx key, leforward key) +- name: verify certmaint receiver key exists + copy: + src: "users/certmaint/" + dest: "{{ getent_passwd[item][4] }}" + mode: 0600 + owner: "{{ item }}" + directory_mode: 0700 + loop: + - certmaint + +- name: allow certmaint group to sudo reload relevant services + lineinfile: + path: /etc/sudoers.d/certmaint_reloads + regexp: "^%certmaint" + line: "%certmaint ALL = (root) NOPASSWD: /usr/sbin/service postfix reload, /usr/sbin/service dovecot reload, /usr/sbin/service nginx reload" + create: yes + mode: 0440 -- cgit v1.2.3