From 9ce9610965ef42f1fd32b42ce75faa6d0db582b4 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 18:08:41 -0400 Subject: [PATCH] fix(debyltech-cloud): external signers, proxy trust, light-only mail - Enable LibreSign's email identify method (click-to-sign, no account creation), remove the stamp background and collect signer metadata. On Skudak these were only ever set in the admin UI. Without the email method a fresh instance answers "No signers." for any outside address. The verify script now asserts it. - Store add_footer=true explicitly. The code already defaults to it, but the 14.2 admin page shows unset as unchecked. - trusted_proxies = the container's own address, read per deploy. Behind rootless port forwarding every request arrives from it, so without this X-Forwarded-For was ignored and every client shared one IP for brute-force throttling. - maintenance_window_start and default_phone_region, which clears the setup warnings. - Mail declares color-scheme "light only" so Apple Mail's dark mode doesn't repaint the white ground and bury the black wordmark (asserted in verify). - Idempotency: redis image fully qualified (docker.io/library/...), the debyltechmail copy owned by the mapped www-data uid, and theming compared before setting. Co-Authored-By: Claude Opus 5.5 --- .../lib/Mail/DebyltechEMailTemplate.php | 15 ++++ .../tasks/containers/debyltech/cloud.yml | 73 +++++++++++++++++++ .../nextcloud/debyltechmail-verify.php.j2 | 18 +++++ 3 files changed, 106 insertions(+) diff --git a/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php b/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php index 97b3f13..5ba9b16 100644 --- a/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php +++ b/ansible/roles/podman/files/debyltechmail/lib/Mail/DebyltechEMailTemplate.php @@ -188,6 +188,21 @@ class DebyltechEMailTemplate extends EMailTemplate { 'font-size:26px;font-weight:300;letter-spacing:-0.02em', $this->heading, ); + + // Opt out of mail-client dark mode. Without this Apple Mail repaints + // the white ground charcoal on its own, and the black wordmark all but + // disappears. Swapping to a white logo under prefers-color-scheme is + // NOT a fix: with Apple Mail's "Use dark backgrounds for messages" off, + // the query still matches while the ground stays white, leaving a + // white-on-white logo. This mail is designed light; render it light. + $this->head = str_replace( + '', + '' + . '' + . '' + . '', + $this->head, + ); } /** diff --git a/ansible/roles/podman/tasks/containers/debyltech/cloud.yml b/ansible/roles/podman/tasks/containers/debyltech/cloud.yml index aea8dfb..69f983b 100644 --- a/ansible/roles/podman/tasks/containers/debyltech/cloud.yml +++ b/ansible/roles/podman/tasks/containers/debyltech/cloud.yml @@ -370,6 +370,58 @@ register: debyltech_enum_fullmatch changed_when: "'is now set to' in debyltech_enum_fullmatch.stdout" +# Settings Skudak only ever had from clicks in the LibreSign admin page, never +# in git -- a fresh instance without them cannot invite anyone by address: +# +# identify_methods The EMAIL identification method. Without it the signer +# search only lists accounts, so an outside address +# returns a bare "No signers." -- the whole point of this +# instance. clickToSign (no emailed code) and +# can_create_account=false, as on Skudak: the emailed link +# is the identity check, and customers never get accounts. +# signature_background_type=deleted +# Drops the LibreSign logo watermark from behind the +# stamp, so with GRAPHIC_ONLY the stamp is the drawn mark +# and nothing else. +# collect_metadata Records signer IP/user agent alongside each signature. +- name: set libresign signer identification and stamp settings in debyltech-cloud + become: true + become_user: "{{ podman_user }}" + ansible.builtin.command: > + podman exec -u www-data debyltech-cloud + php occ config:app:set libresign {{ item.k }} --value={{ item.v | quote }} + register: debyltech_libresign_settings + changed_when: "'is now set to' in debyltech_libresign_settings.stdout" + loop: + - k: identify_methods + v: >- + {{ [{'name': 'email', 'friendly_name': 'Email', 'enabled': true, + 'mandatory': true, + 'signatureMethods': { + 'clickToSign': {'name': 'clickToSign', 'enabled': true}, + 'emailToken': {'name': 'emailToken', 'enabled': false}}, + 'can_create_account': false, + 'test_url': '/index.php/settings/admin/mailtest', + 'signatureMethodEnabled': 'clickToSign'}] | to_json }} + - {k: signature_background_type, v: deleted} + - {k: collect_metadata, v: "1"} + loop_control: + label: "{{ item.k }}" + +# The validation footer ("Digitally signed by ... Validate in ") is WANTED +# -- only its QR code goes, below. FooterHandler defaults add_footer to true +# when unset, but the 14.2 admin page renders unset as UNCHECKED, inviting +# someone to "correct" it into actually turning the footer off. Stored +# explicitly so the page shows what the code does. +- name: keep libresign validation footer text in debyltech-cloud + become: true + become_user: "{{ podman_user }}" + ansible.builtin.command: > + podman exec -u www-data debyltech-cloud + php occ config:app:set libresign add_footer --value=1 --type=boolean + register: libresign_footer + changed_when: "'is now set to' in libresign_footer.stdout" + - name: drop libresign validation QR code from signed-PDF footer in debyltech-cloud become: true become_user: "{{ podman_user }}" @@ -444,6 +496,23 @@ register: debyltechmail_enable changed_when: "'already enabled' not in debyltechmail_enable.stdout" +# Behind rootless podman's port forwarder, every request -- Caddy's included -- +# reaches Apache FROM THE CONTAINER'S OWN ADDRESS on `shared`, not from the +# host. Unless exactly that address is a trusted proxy, Nextcloud ignores the +# X-Forwarded-For header Caddy sends, so every client looks like one IP: +# brute-force throttling then penalises everyone at once. Read per deploy +# because the address is assigned at container creation, and deploys are the +# only thing that recreate it. +- name: read debyltech-cloud container address + become: true + become_user: "{{ podman_user }}" + ansible.builtin.command: > + podman inspect debyltech-cloud + --format "{{ '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' }}" + register: debyltech_cloud_ip + changed_when: false + failed_when: debyltech_cloud_ip.stdout is not match('^[0-9.]+$') + # System config, set only when it differs so a clean re-deploy reports no # changes (Skudak's equivalents report changed on every run). Values are # single-quoted into the shell, so the backslashes in mail_template_class pass @@ -467,6 +536,10 @@ loop: - {k: overwrite.cli.url, v: "https://{{ cloud_debyltech_server_name }}"} - {k: overwriteprotocol, v: https} + - {k: trusted_proxies 0, v: "{{ debyltech_cloud_ip.stdout }}"} + # Hour in UTC: 05:00 UTC is 01:00/00:00 Eastern, ahead of the 04:15 backup. + - {k: maintenance_window_start, v: "5", t: integer} + - {k: default_phone_region, v: US} - {k: loglevel, v: "2", t: integer} - {k: log_rotate_size, v: "10485760", t: integer} - {k: mail_template_class, v: "OCA\\Debyltechmail\\Mail\\DebyltechEMailTemplate"} diff --git a/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2 b/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2 index cac2f3b..65720f8 100644 --- a/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2 +++ b/ansible/roles/podman/templates/nextcloud/debyltechmail-verify.php.j2 @@ -72,6 +72,10 @@ if (!str_contains($text, 'official document-signing request')) { if (!str_contains($html, 'debyltech.com/legal/privacy') || !str_contains($html, 'debyltech.com/legal/tos')) { $failures[] = 'privacy/terms links missing from footer'; } +if (!str_contains($html, 'content="light only"')) { + $failures[] = 'color-scheme "light only" meta missing -- dark-mode mail clients will repaint ' + . 'the ground and bury the black wordmark (did upstream rename in $head?)'; +} if (preg_match('/[»«]/u', $html)) { $failures[] = 'German guillemets survived into the body'; } @@ -131,6 +135,20 @@ if ($appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_ . 'unset or "yes"; it is NOT the knob for the account-owned-email problem.'; } +// Outside signers are invited by address; without an enabled email identify +// method the signer search returns "No signers." for any non-account email. +$methods = json_decode($appConfig->getValueString('libresign', 'identify_methods', '[]'), true) ?: []; +$emailOn = false; +foreach ($methods as $m) { + if (($m['name'] ?? '') === 'email' && !empty($m['enabled'])) { + $emailOn = true; + } +} +if (!$emailOn) { + $failures[] = 'libresign email identify method is not enabled -- outside addresses ' + . 'cannot be added as signers ("No signers.")'; +} + $identDocs = $appConfig->getValueString('libresign', 'identification_documents', ''); if ($identDocs !== '0') { $failures[] = 'libresign identification_documents is "' . $identDocs