ansible-taler-exchange

Ansible playbook to deploy a production Taler Exchange
Log | Files | Refs | README | LICENSE

commit 2dee4146e911b3e8b51e06384a695bbd94f3c45d
parent 77ec42ed423197a5b71f9489785045b724e69f93
Author: Florian Dold <dold@taler.net>
Date:   Sun,  9 Aug 2026 17:09:52 +0200

Add Rusty mock MFA delivery

Diffstat:
Minventories/group_vars/all/defaults.yml | 5+++++
Minventories/host_vars/rusty/vars.yml | 5+++++
Mplaybooks/setup.yml | 9+++++++++
Aroles/challenger/files/usr/local/bin/mock-mfa-email | 34++++++++++++++++++++++++++++++++++
Aroles/challenger/files/usr/local/bin/mock-mfa-sms | 34++++++++++++++++++++++++++++++++++
Mroles/challenger/tasks/pre-exchange.yml | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mroles/challenger/templates/etc/challenger/challenger-email.conf.j2 | 4++++
Mroles/challenger/templates/etc/challenger/challenger-sms.conf.j2 | 4++++
Mroles/exchange/templates/etc/nginx/sites-available/exchange-nginx.conf.j2 | 8++++++++
9 files changed, 163 insertions(+), 0 deletions(-)

diff --git a/inventories/group_vars/all/defaults.yml b/inventories/group_vars/all/defaults.yml @@ -18,6 +18,11 @@ ebics_keys_external: false # deployments under any circumstance. dangerously_enable_devtesting: false +# Publish MFA challenge messages for development testing. This is intentionally +# disabled by default and additionally guarded in playbooks/setup.yml so it can +# only run on the Rusty staging host. +devtesting_mock_mfa: false + # Configure EBICS. # When only this setting is enabled, # the EBICS config is only deployed, diff --git a/inventories/host_vars/rusty/vars.yml b/inventories/host_vars/rusty/vars.yml @@ -73,6 +73,11 @@ exchange_sanction_helper: taler-exchange-helper-sanctions-dummy # deployments under any circumstance. dangerously_enable_devtesting: true +# MFA challenge messages are written to /var/www/mock-mfa and served at +# https://exchange.{{ domain_name }}/mock-mfa/. Never enable this on a +# production system. +devtesting_mock_mfa: true + devtesting_ssh_keys: - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINHSjJ/zPwQnqBrKp0qK+OdsZYfQ8DHY2dyJakNozBi7 fdold-work@sapota" - "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoXKfSbPriOPoFRR+lMAJszH0/7jaPZOxdg85/URlRbe0ljm8fxbVmup1EjGPuKKJkyYqJIqGQCRHPNYeBt05APXYEO+4d5WAuPY6QOiTFGxB5RueWHAjFM5LVKtBH9Ozln+ngjeXlID48ueuBY2LO24hRuZtRmHYKN1AwQNA2XKtjteKINx99ljm3uwVV9IDYAJkRWKllolLrSFfqK6CHDS/IqlMNp3qNNhNXEW+/Vm5kMUPzKvhPXH/OsFr2KyKaO/+zVXptwje9imtaYaD5iEuRbEfP+6OsCKKpIlp6kyfOUPLuxK+RQfDRY3pyHeCKGriv3DGUpCYqtFVZlmsww== stibane@feh.com" diff --git a/playbooks/setup.yml b/playbooks/setup.yml @@ -13,6 +13,15 @@ that: exchange_attribute_encryption_key is defined quiet: true + - name: Restrict mock MFA to the Rusty staging host + ansible.builtin.assert: + that: inventory_hostname == 'rusty' + fail_msg: >- + devtesting_mock_mfa exposes MFA codes and may only be enabled on + the Rusty staging host. + quiet: true + when: devtesting_mock_mfa | bool + - name: "Check the KYCAID secrets" when: deployment_kind == 'tops' ansible.builtin.assert: diff --git a/roles/challenger/files/usr/local/bin/mock-mfa-email b/roles/challenger/files/usr/local/bin/mock-mfa-email @@ -0,0 +1,34 @@ +#!/bin/sh +# Development-only email challenger delivery helper. Challenger provides the +# email address as $1 and the MFA message on standard input. +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: mock-mfa-email EMAIL_ADDRESS" >&2 + exit 64 +fi + +# Keep real deliveries real. Only the reserved staging email addresses +# documented for MyTOPS development testing are captured locally. +case "$1" in + test-[0-9][0-9]@taler.net) + ;; + *) + exec /usr/bin/challenger-send-email "$@" + ;; +esac + +message_dir=/var/www/mock-mfa +# Keep email-address filenames readable while preventing a supplied address +# from escaping the message directory. +filename=$(LC_ALL=C printf '%s' "$1" | tr -c '[:alnum:]@+._-' '_') + +# Files must be readable by nginx through the challenger-mock-mfa group. Write +# atomically so the web server never exposes a partially-written MFA message. +umask 007 +tmpfile=$(mktemp "$message_dir/.${filename}.XXXXXX") +trap 'rm -f "$tmpfile"' EXIT HUP INT TERM +chmod 0640 "$tmpfile" +cat >"$tmpfile" +mv -f "$tmpfile" "$message_dir/$filename.txt" +trap - EXIT HUP INT TERM diff --git a/roles/challenger/files/usr/local/bin/mock-mfa-sms b/roles/challenger/files/usr/local/bin/mock-mfa-sms @@ -0,0 +1,34 @@ +#!/bin/sh +# Development-only SMS challenger delivery helper. Challenger provides the +# phone number as $1 and the MFA message on standard input. +set -eu + +if [ "$#" -ne 1 ]; then + echo "usage: mock-mfa-sms PHONE_NUMBER" >&2 + exit 64 +fi + +# Keep real deliveries real. Only the reserved staging phone numbers +# documented for MyTOPS development testing are captured locally. +case "$1" in + +417000000[0-9][0-9]) + ;; + *) + exec /usr/bin/challenger-send-sms "$@" + ;; +esac + +message_dir=/var/www/mock-mfa +# Keep phone-number filenames readable while preventing a supplied number from +# escaping the message directory. +filename=$(LC_ALL=C printf '%s' "$1" | tr -c '[:alnum:]@+._-' '_') + +# Files must be readable by nginx through the challenger-mock-mfa group. Write +# atomically so the web server never exposes a partially-written MFA message. +umask 007 +tmpfile=$(mktemp "$message_dir/.${filename}.XXXXXX") +trap 'rm -f "$tmpfile"' EXIT HUP INT TERM +chmod 0640 "$tmpfile" +cat >"$tmpfile" +mv -f "$tmpfile" "$message_dir/$filename.txt" +trap - EXIT HUP INT TERM diff --git a/roles/challenger/tasks/pre-exchange.yml b/roles/challenger/tasks/pre-exchange.yml @@ -79,6 +79,66 @@ system: true state: present +- name: Ensure mock MFA group exists + ansible.builtin.group: + name: challenger-mock-mfa + state: present + when: devtesting_mock_mfa | bool + +- name: Grant challenger services and nginx access to mock MFA messages + ansible.builtin.user: + name: "{{ item }}" + groups: challenger-mock-mfa + append: true + loop: + - challenger-sms + - challenger-email + - challenger-postal + - www-data + notify: Restart nginx + when: devtesting_mock_mfa | bool + +- name: Ensure mock MFA message directory exists + ansible.builtin.file: + path: /var/www/mock-mfa + state: directory + owner: root + group: challenger-mock-mfa + mode: "2770" + when: devtesting_mock_mfa | bool + +- name: Remove mock MFA messages when mock MFA is disabled + ansible.builtin.file: + path: /var/www/mock-mfa + state: absent + when: not (devtesting_mock_mfa | bool) + +- name: Install mock MFA delivery helpers + ansible.builtin.copy: + src: "usr/local/bin/mock-mfa-{{ item }}" + dest: "/usr/local/bin/mock-mfa-{{ item }}" + owner: root + group: root + mode: "0755" + loop: + - sms + - email + when: devtesting_mock_mfa | bool + +- name: Remove mock MFA delivery helpers when mock MFA is disabled + ansible.builtin.file: + path: "/usr/local/bin/mock-mfa-{{ item }}" + state: absent + loop: + - sms + - email + when: not (devtesting_mock_mfa | bool) + +- name: Remove obsolete generic mock MFA helper + ansible.builtin.file: + path: /usr/local/bin/mock-mfa + state: absent + - name: Ensure Ansible facts directory exists ansible.builtin.file: path: "/etc/ansible/facts.d/" diff --git a/roles/challenger/templates/etc/challenger/challenger-email.conf.j2 b/roles/challenger/templates/etc/challenger/challenger-email.conf.j2 @@ -12,7 +12,11 @@ UNIXPATH_MODE = 666 # Which external command should be used to transmit challenges? # Example commands are challenger-send-{sms,email,post} +{% if devtesting_mock_mfa | bool %} +AUTH_COMMAND = /usr/local/bin/mock-mfa-email +{% else %} AUTH_COMMAND = /usr/bin/challenger-send-email +{% endif %} # How long is an individual validation request valid? VALIDATION_DURATION = 7d diff --git a/roles/challenger/templates/etc/challenger/challenger-sms.conf.j2 b/roles/challenger/templates/etc/challenger/challenger-sms.conf.j2 @@ -12,7 +12,11 @@ UNIXPATH_MODE = 666 # Which external command should be used to transmit challenges? # Example commands are challenger-send-{sms,email,post} +{% if devtesting_mock_mfa | bool %} +AUTH_COMMAND = /usr/local/bin/mock-mfa-sms +{% else %} AUTH_COMMAND = /usr/bin/challenger-send-sms +{% endif %} # Name of a file with the message to send with the challenge. MESSAGE_TEMPLATE_FILE = /etc/challenger/sms-message-template.txt diff --git a/roles/exchange/templates/etc/nginx/sites-available/exchange-nginx.conf.j2 b/roles/exchange/templates/etc/nginx/sites-available/exchange-nginx.conf.j2 @@ -31,4 +31,12 @@ server { alias /var/www/downloads/; autoindex off; } + +{% if devtesting_mock_mfa | bool %} + # Development testing only: expose the MFA messages written by mock-mfa. + location /mock-mfa/ { + alias /var/www/mock-mfa/; + autoindex on; + } +{% endif %} }