diff --git a/roles/nextcloud-db-cert/files/fetch-cert.py b/roles/nextcloud-db-cert/files/fetch-cert.py new file mode 100644 index 0000000..c92ab0c --- /dev/null +++ b/roles/nextcloud-db-cert/files/fetch-cert.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import base64 +import json +import logging +import os +import ssl +import urllib.error +import urllib.request +from pathlib import Path + +try: + from systemd.journal import JournalHandler as LogHandler +except ModuleNotFoundError: + LogHandler = logging.StreamHandler + + +log = logging.getLogger('fetchcert') + + +CREDENTIALS_DIRECTORY = Path(os.environ['CREDENTIALS_DIRECTORY']) + +CA_FILE = Path('/etc/pki/ca-trust/kube-root-ca.crt') +CERT_OUT = Path('/etc/nextcloud/postgresql.cer') +KEY_OUT = Path('/etc/nextcloud/postgresql.key') + +BASE_URL = 'https://kubernetes.pyrocufflink.blue:6443' +NAMESPACE = 'postgresql-ca' +SECRET = 'nextcloud-client' + + +def main(): + if 'LOG_LEVEL' in os.environ: + logging.root.setLevel(os.environ['LOG_LEVEL'].upper()) + logging.root.addHandler(LogHandler()) + + token_path = CREDENTIALS_DIRECTORY / 'nextcloud.fetchcert.token' + log.debug('Reading token from %s', token_path) + token = token_path.read_text().rstrip() + + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.load_verify_locations(cafile=CA_FILE) + + url = f'{BASE_URL}/api/v1/namespaces/{NAMESPACE}/secrets/{SECRET}' + headers = { + 'Authorization': f'Bearer {token}' + } + req = urllib.request.Request(url, headers=headers) + log.info('Fetching Secret from %s', url) + try: + res = urllib.request.urlopen(req, context=ctx) + except urllib.error.HTTPError as e: + log.error('%s %s', e, e.read()) + raise SystemExit(1) + with res: + body = json.load(res) + log.info('Received HTTP reponse %d %s', res.status, res.reason) + cert = base64.b64decode(body['data']['tls.crt'].encode('ascii')) + key = base64.b64decode(body['data']['tls.key'].encode('ascii')) + cert_new = CERT_OUT.with_suffix(f'{CERT_OUT.suffix}.new') + key_new = KEY_OUT.with_suffix(f'{KEY_OUT.suffix}.new') + if not CERT_OUT.exists() or CERT_OUT.read_bytes() != cert: + log.debug('Writing certificate to %s', cert_new) + with cert_new.open('wb') as f: + os.fchown(f.fileno(), 0, 48) + os.fchmod(f.fileno(), 0o0444) + f.write(cert) + if not KEY_OUT.exists() or KEY_OUT.read_bytes() != key: + log.debug('Writing private key to %s', key_new) + with key_new.open('wb') as f: + os.fchown(f.fileno(), 0, 48) + os.fchmod(f.fileno(), 0o0440) + f.write(key) + if cert_new.exists(): + log.debug('Renaming %s to %s', cert_new, CERT_OUT) + cert_new.rename(CERT_OUT) + if key_new.exists(): + log.debug('Renaming %s to %s', key_new, KEY_OUT) + key_new.rename(KEY_OUT) + log.info('Successfully fetched certificate from Kubernetes Secret') + + +if __name__ == '__main__': + main() diff --git a/roles/nextcloud-db-cert/files/nextcloud-fetch-cert.service b/roles/nextcloud-db-cert/files/nextcloud-fetch-cert.service new file mode 100644 index 0000000..0e8f7af --- /dev/null +++ b/roles/nextcloud-db-cert/files/nextcloud-fetch-cert.service @@ -0,0 +1,17 @@ +[Unit] +Description=Fetch Nextcloud database client certificate +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +Environment=LOG_LEVEL=debug +ExecStart=/usr/local/libexec/nextcloud-fetch-cert.py +LoadCredential=nextcloud.fetchcert.token +CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_CHOWN +PrivateTmp=yes +ProtectHome=yes +ProtectKernelTunables=yes +ProtectProc=invisible +ProtectSystem=full +ReadWritePaths=/etc/nextcloud diff --git a/roles/nextcloud-db-cert/handlers/main.yml b/roles/nextcloud-db-cert/handlers/main.yml new file mode 100644 index 0000000..c54df98 --- /dev/null +++ b/roles/nextcloud-db-cert/handlers/main.yml @@ -0,0 +1,8 @@ +- name: reload systemd + systemd: + daemon_reload: true + +- name: restart nextcloud-fetch-cert.timer + systemd: + name: nextcloud-fetch-cert.timer + state: restarted diff --git a/roles/nextcloud-db-cert/tasks/main.yml b/roles/nextcloud-db-cert/tasks/main.yml new file mode 100644 index 0000000..20d9a0d --- /dev/null +++ b/roles/nextcloud-db-cert/tasks/main.yml @@ -0,0 +1,71 @@ +- name: ensure nextcloud db cert fetch script is installed + copy: + src: fetch-cert.py + dest: /usr/local/libexec/nextcloud-fetch-cert.py + owner: root + group: root + mode: u=rwx,go=rx + notify: + - restart nextcloud-fetch-cert.timer + tags: + - copy-script + +- name: ensure nextcloud db cert fetch token credential exists + copy: + dest: /etc/credstore/nextcloud.fetchcert.token + content: |+ + {{ nextcloud_fetchcert_token }} + owner: root + group: root + mode: u=rw,go= + diff: false + tags: + - credentials + +- name: ensure kubernetes ca certificate is installed + copy: + src: kube-root-ca.crt + dest: /etc/pki/ca-trust/kube-root-ca.crt + owner: root + group: root + mode: u=rw,go=r + tags: + - cacert + +- name: ensure nextcloud cert fetch timer unit is installed + template: + src: nextcloud-fetch-cert.timer.j2 + dest: /etc/systemd/system/nextcloud-fetch-cert.timer + owner: root + group: root + mode: u=rw,go=r + notify: + - reload systemd + - restart nextcloud-fetch-cert.timer + tags: + - systemd +- name: ensure nextcloud cert fetch service unit is installed + copy: + src: nextcloud-fetch-cert.service + dest: /etc/systemd/system/nextcloud-fetch-cert.service + owner: root + group: root + mode: u=rw,go=r + notify: + - reload systemd + - restart nextcloud-fetch-cert.timer + tags: + - systemd + +- name: ensure nextcloud cert fetch timer is enabled + systemd: + name: nextcloud-fetch-cert.timer + enabled: true + tags: + - service +- name: ensure nextcloud cert fetch timer is started + systemd: + name: nextcloud-fetch-cert.timer + state: started + tags: + - service diff --git a/roles/nextcloud-db-cert/templates/nextcloud-fetch-cert.timer.j2 b/roles/nextcloud-db-cert/templates/nextcloud-fetch-cert.timer.j2 new file mode 100644 index 0000000..2e1b6db --- /dev/null +++ b/roles/nextcloud-db-cert/templates/nextcloud-fetch-cert.timer.j2 @@ -0,0 +1,9 @@ +[Unit] +Description=Periodically fetch Nextcloud database client certificate + +[Timer] +OnActiveSec=1s +OnUnitInactiveSec=12h + +[Install] +WantedBy=timers.target