ansible-taler-exchange

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

README (24524B)


      1 # Ansible Taler Playbooks
      2 
      3 ## Installing dependencies
      4 
      5 Depending on your local installation, you might need
      6 to install the following ansible collection:
      7 
      8 ```
      9 $ ansible-galaxy collection install community.postgresql
     10 ```
     11 
     12 Playbooks that touch vaulted variables need the vault password in
     13 "vault_pass.txt", which is gitignored and shipped only as
     14 "vault_pass.txt.gpg":
     15 
     16 ```
     17 $ gpg -d vault_pass.txt.gpg > vault_pass.txt
     18 ```
     19 
     20 ## Running the main Playbooks
     21 
     22 The canonical playbooks are run via shell scripts in the top-level
     23 directory.
     24 
     25 The operational helpers pass arguments after their positional arguments
     26 directly to `ansible-playbook`. For example:
     27 
     28 ```
     29 $ ./deploy spec --check --diff
     30 $ ./sanction-check spec sanctions.txt --diff
     31 $ BORG_PASSPHRASE=... ./restore spec archive-name --verbose
     32 ```
     33 
     34 Put the host/group first, followed by the sanction list or restore archive
     35 where required, then any Ansible options. `extract-borg-key` and
     36 `setup-pixel-borg` default to `spec` when called without arguments; supply
     37 an explicit target when passing options. Quote option values containing
     38 spaces as usual. Check-mode support depends on the playbook; restore
     39 explicitly rejects check mode. The `test` runner and `stage-activate` do
     40 not forward Ansible options.
     41 
     42 ### Local Podman deployment test
     43 
     44 Run `./test` to build a Debian Trixie container, provision the complete test
     45 deployment, repeat `setup.yml` with `--check --diff`, and exercise the main
     46 systemd units and HTTP APIs.  The script creates a disposable SSH key and
     47 removes the container when it exits.
     48 
     49 The container deliberately runs privileged because the hardened Taler systemd
     50 units need capabilities which Podman otherwise removes.  It also uses the
     51 pregenerated DH parameters and a container-only Certbot substitute which
     52 creates short-lived self-signed certificates: public ACME validation cannot
     53 reach the local test domains.  Neither shortcut is used by real deployments.
     54 
     55 ### Main setup (restore, deploy)
     56 
     57 Database restore is deliberately separate from normal deployment.  It is a
     58 fresh-host recovery operation, not an idempotent part of `deploy`, and it
     59 never chooses an archive implicitly.  First list the Borg repository and
     60 select the exact archive to restore:
     61 
     62 ```
     63 $ BORG_PASSPHRASE=... borg list ssh://borg@pixel.taler-systems.com/~/spec-backup
     64 $ BORG_PASSPHRASE=... ./restore spec 'archive-name-copied-from-list'
     65 ```
     66 
     67 The first argument must be one exact inventory hostname (not a group or host
     68 pattern).  The destination PostgreSQL cluster must contain only the standard
     69 `postgres` database and role.  The restore validates the compressed
     70 `root/postgres-backup.sql.gz` artifact, rejects dumps made by a newer
     71 PostgreSQL major version, restores all databases and roles, and verifies the
     72 exchange database before reporting success.  It does not deploy or start the
     73 application; inspect the recovered host and then run `deploy` separately.
     74 
     75 If SQL restore or verification fails, the partially restored cluster is
     76 considered tainted and must be reinitialized before another attempt.  The
     77 temporary database dump is removed, while a private error log is retained in
     78 `/var/tmp/taler-postgres-restore-*.log` on the target for diagnosis.
     79 
     80 The "deploy" script deploys the latest version of a system on a host.
     81 If you are root@rusty.taler-ops.ch, you may be able to:
     82 
     83 ```
     84 $ ./deploy rusty
     85 ```
     86 
     87 Deployments keep `/var/lib/taler-deployment/incomplete` until configuration
     88 handlers and post-deployment checks have succeeded. If a run fails or is
     89 interrupted, the next `deploy` reapplies nginx and enabled monitoring service
     90 configuration even when the files are unchanged. This recovers restart
     91 notifications lost by the previous Ansible process. Applications still stay
     92 stopped on deployment failure; check mode neither clears the marker nor
     93 restarts services. Normal successful reruns keep the usual change-triggered
     94 handler behavior.
     95 
     96 For TOPS production, replace the "rusty" with "spec" to use the actual secrets
     97 for the deployment. For this, you first need to decrypt them:
     98 
     99 ```
    100 $ ./contrib/decrypt inventories/host_vars/spec/prod-secrets.yml.gpg
    101 ```
    102 
    103 Make sure to NEVER commit the decrypted production secrets to Git.
    104 Instead, if you had to edit them, re-encrypt them to all admins:
    105 
    106 ```
    107 $ ./contrib/encrypt inventories/host_vars/spec/prod-secrets.yml
    108 ```
    109 
    110 ### Staging offline master key
    111 
    112 The `test-master.priv` offline master key is intentionally committed to this
    113 repository and referenced by `stage-offline.conf`.  It is a disposable test key
    114 for the staging exchange, where allowing repository users to sign staging
    115 configuration is intentional.  It must never be used for production, for an
    116 exchange holding funds of value, or as the basis for any security assumption.
    117 
    118 ### sanction-check
    119 
    120 This command imports and checks the latest sanction lists:
    121 
    122 ```
    123 $ ./sanction-check $TARGET $LIST
    124 ```
    125 
    126 where "$TARGET" is a host or group from "inventories/default" and $LIST
    127 is the path of the sanction list file on the local disk.
    128 
    129 Note that "SANCTION_LIST" means something else in the "exchange" role,
    130 where it is the name of a file under
    131 "roles/exchange/files/var/lib/taler-exchange/" that is deployed to the
    132 host and referenced from the exchange configuration.
    133 
    134 NOTE: this should still be further automated.
    135 
    136 
    137 ### External node monitoring
    138 
    139 To monitor an exchange node through `tsys-infra/monitoring`, first enroll it
    140 as an external monitoring client using that repository's
    141 `monitoring/README-external.md`. Retrieve both exported YAML files through
    142 an authenticated, encrypted channel. This repository configures only the
    143 exchange node; it does not connect to Sentol or enroll hosts itself.
    144 
    145 Place the exports in the exchange inventory host directory:
    146 
    147 ```
    148 inventories/host_vars/$HOST/monitoring-client.yml
    149 inventories/host_vars/$HOST/monitoring-client-secrets.yml
    150 ```
    151 
    152 The directory uses this repository's inventory hostname, such as `spec`.
    153 The identity inside the export remains the identity enrolled on Sentol;
    154 it need not equal that inventory alias. Use the complete exports without
    155 renaming their fields or combining them with other host variables.
    156 
    157 The proxy bind address defaults to `127.0.0.1` when omitted from the bundle.
    158 Set `monitoring_client.node_exporter.proxy_bind_address` to `"*"` (quoted
    159 in YAML) to listen on all IPv4 and IPv6 interfaces. This requires IPv6
    160 support on the node. Alternatively, use a literal IPv4 or IPv6 address
    161 assigned to the exchange node and reachable from the monitoring server.
    162 Hostnames and bare wildcard addresses (`0.0.0.0` and `::`) are rejected;
    163 use `"*"` for both address families. With `"*"`, the proxy and loopback
    164 backend must use different ports (normally 9100 and 9101). The proxy
    165 still requires mTLS, and the backend remains loopback-only. Keep the
    166 enrolled certificate identity unchanged.
    167 
    168 Commit both files, encrypting the entire secrets file with Ansible Vault
    169 before staging it:
    170 
    171 ```
    172 $ chmod 0600 inventories/host_vars/$HOST/monitoring-client-secrets.yml
    173 $ ansible-vault encrypt inventories/host_vars/$HOST/monitoring-client-secrets.yml
    174 ```
    175 
    176 Ansible uses the existing `vault_pass.txt` configuration. The committed
    177 secrets file must begin with `$ANSIBLE_VAULT;`; plaintext secrets are
    178 rejected when monitoring is enabled. Do not commit the plaintext export
    179 or the vault password. The Ansible controller needs Python 3 and OpenSSL
    180 for bundle validation. As with other vaulted host variables, Ansible may
    181 need the vault password even when monitoring management is disabled.
    182 
    183 Set this in the host's public inventory configuration, then deploy normally:
    184 
    185 ```yaml
    186 enable_monitoring: true
    187 ```
    188 
    189 ```
    190 $ ./deploy "$HOST"
    191 ```
    192 
    193 The `monitoring` role installs Debian's node_exporter with a loopback-only
    194 listener and systemd metrics, a dedicated `node-exporter-proxy` nginx
    195 service requiring Sentol's mTLS client identity, and buffered Fluent Bit
    196 JSON Lines forwarding over HTTPS/mTLS. All connection settings and certificate identities
    197 come from the bundle. TLS material lives in `/etc/taler-monitoring/tls`;
    198 private keys and the enclosing directory are accessible only to root.
    199 The root-running Fluent Bit service reads the journal and client key. The role
    200 requires Debian 13 amd64/arm64 and installs pinned Fluent Bit 5.1.2 from its signed
    201 upstream APT repository. The bundle declares `logs.protocol: jsonline` and an HTTPS
    202 `logs.url` ending in `/jsonline`; old RELP bundles fail preflight.
    203 
    204 Allow Sentol to reach the exported metrics proxy address and port
    205 (normally TCP 9100), and allow the node to reach the exported HTTPS log server
    206 (normally TCP 2514). Firewall and routing configuration is external to
    207 this role. The dedicated proxy leaves exchange webserver sites intact.
    208 
    209 Verify the local services with:
    210 
    211 ```
    212 $ systemctl is-active prometheus-node-exporter node-exporter-proxy fluent-bit
    213 $ curl --fail http://127.0.0.1:9101/metrics >/dev/null
    214 $ logger --tag external-monitoring-enrollment 'Exchange monitoring test'
    215 ```
    216 
    217 Use the exported backend address if it differs. Follow the external-client
    218 runbook to perform an authenticated scrape from Sentol, confirm anonymous
    219 scrapes fail, and find the test message in VictoriaLogs using the exported identity as
    220 `authenticated_host`. Fluent Bit metrics are available through the same mTLS proxy
    221 at `/fluent-bit/metrics`.
    222 
    223 Fluent Bit reads only new journal entries on first activation and resumes its saved
    224 cursor on later starts. Journald uses persistent storage without changing host-specific
    225 retention. `/var/lib/fluent-bit` retains the cursor and filesystem buffer, with a
    226 1 GiB output queue by default (`monitoring_fluent_bit_queue_limit`). Retryable failures
    227 are retried without a count limit; a full queue discards its oldest chunks. Permanent
    228 HTTP errors can discard records, and VictoriaLogs can accept a batch while rejecting
    229 individual malformed lines. Watch both collector drop counters and receiver errors.
    230 Lost responses can cause duplicates; this is not exactly-once or unconditional
    231 lossless delivery. Fluent Bit's own logs remain local to prevent feedback loops.
    232 
    233 For renewal, retrieve both refreshed exports after `make deploy-sentol`,
    234 replace the local files, encrypt the complete new secrets file before
    235 staging it, commit both, and run `deploy` again. Do not reuse an old key
    236 with a renewed certificate. The role validates both pairs before deployment
    237 and reloads or restarts the affected services when their material changes.
    238 
    239 `enable_monitoring` defaults to false. False skips management and leaves
    240 installed monitoring running. To offboard, follow the Sentol runbook and,
    241 on the exchange node, stop and disable `node-exporter-proxy` and
    242 `prometheus-node-exporter` and `fluent-bit`, then remove the proxy unit
    243 and `/etc/taler-monitoring` (including its private keys) and run
    244 `systemctl daemon-reload`. Set the flag false and remove both inventory
    245 exports. This does not revoke previously issued certificates; complete
    246 the Sentol offboarding steps as well.
    247 
    248 ### Setting up backups (TOPS-only for now)
    249 
    250 First run:
    251 
    252 ```
    253 $ ./extract-borg-key $DEPLOYMENT
    254 ```
    255 
    256 The resulting SSH public key should be added to the borg-account
    257 of the host storing the backup. The borg_host inventory setting selects the target
    258 hostname.
    259 
    260 Once the SSH key is deployed and the backup has been initialized
    261 server-side (see admin-logs/pixel/03-borg.txt), start the daily
    262 backups via:
    263 
    264 ```
    265 $ ./start-borg-backups $DEPLOYMENT
    266 ```
    267 
    268 This will make a backup basically everything relevant to the
    269 deployment, **except** the exchange online signing keys. The
    270 backup will in particular include the system configuration
    271 and a full (gzip-compressed) snapshot of the database.  Thus,
    272 the backups should also suffice to diagnose problems.
    273 
    274 Backups are set to retain daily snapshots of the last 7 days,
    275 weekly snapshots for the last 4 weeks, and monthly snapshots
    276 for the last 6 months.
    277 
    278 
    279 ### Backup (right now)
    280 
    281 To run a backup "immediately" (instead of the daily regular
    282 backups), use:
    283 
    284 ```
    285 $ ./backup $DEPLOYMENT
    286 ```
    287 
    288 
    289 ### Rebooting (into a new kernel)
    290 
    291 This should be done via the 'reboot' playbook which can
    292 be invoked via the
    293 
    294 ```
    295 $ ./reboot $DEPLOYMENT
    296 ```
    297 
    298 script. The reboot playbook first stops all Taler services,
    299 then makes a backup, and then reboots. This should help us
    300 restore to another system in case the host does not come back
    301 online cleanly.
    302 
    303 
    304 ### Testing Locally
    305 
    306 With podman and ansible installed locally one can run:
    307 
    308 ```
    309 $ ./test
    310 ```
    311 
    312 This will begin building the Containerfile in this repo, which is a Debian
    313 base with systemd and a paswordless ssh server configured. Then container
    314 will start, binding port 8022 to 127.0.0.1 on the host. Finally
    315 the setup playbook will be run on the container via ssh.
    316 
    317 
    318 ## Playbooks
    319 
    320 ### backup
    321 
    322 Runs a backup "right now".
    323 
    324 ### borg-ssh-export
    325 
    326 Exports the SSH public keys needed at the remote host for backups.
    327 
    328 ### borg-start
    329 
    330 Enables the borg backup. Should be run after the SSH public keys
    331 exported via borg-ssh-export have been deployed on the receiving
    332 host.
    333 
    334 ### pixel-borg
    335 
    336 Enables receiving (!) backups from pixel. Adds the public key from
    337 pixel so we accept receiving borg backups from pixel.  Note that
    338 pixel still needs to be setup to send the backups.
    339 
    340 ### reboot
    341 
    342 Safely reboots the system by first stopping all Taler services,
    343 then making a backup and only then actually rebooting it.
    344 
    345 ### sanctionlist-check
    346 
    347 Imports the latest sanction list and checks all records against
    348 it.
    349 
    350 ### setup
    351 
    352 The main script that deploys our entire setup.
    353 
    354 ## Roles
    355 
    356 ### auditor
    357 
    358 Deploys the auditor.
    359 
    360 ### backup
    361 
    362 Runs the backup script, making a borg backup of the database and other key parts of the system.
    363 
    364 ### borg-ssh-export
    365 
    366 Exports the SSH public key that must be deployed on the host that is
    367 to receive the backup.
    368 
    369 ### borg-start
    370 
    371 Deploys the backup script and the cron job that runs it daily.
    372 
    373 ### cert
    374 
    375 Issues or renews a LetsEncrypt certificate with certbot and enables the
    376 nginx sites that use it. Included by the roles that own a vhost, not
    377 listed in a playbook directly.
    378 
    379 ### challenger
    380 
    381 Deploys the various challenger services for address verification.
    382 
    383 ### common_packages
    384 
    385 Installs the base system packages we need on all hosts.
    386 Sets up Taler package repo and installs Taler packages.
    387 
    388 ### database
    389 
    390 Installs PostgreSQL and the dependencies needed to administer it through
    391 Ansible, then ensures the database service is running.  Normal deployment
    392 never restores a backup.
    393 
    394 ### database_restore
    395 
    396 Implements the guarded fresh-cluster restore used only by `playbooks/restore.yml`
    397 and `restore`.
    398 
    399 ### devtesting
    400 
    401 Sets up a restricted account for faking wire transfers and inspecting
    402 challenger auth codes. Gated on "dangerously_enable_devtesting" and never
    403 to be enabled in production.
    404 
    405 ### exchange
    406 
    407 Deploys the Taler exchange, then hands over to "exchange_$DEPLOYMENT_KIND"
    408 for the site-specific configuration.
    409 
    410 ### exchange_tops
    411 
    412 Deployment-specific exchange configuration (denominations, KYC rules, fees,
    413 legal documents), selected by DEPLOYMENT_KIND.
    414 
    415 ### exchange-sanctionlist-import
    416 
    417 Imports a new sanction list and checks all existing records against it.
    418 
    419 ### libeufin-nexus
    420 
    421 Deploys libeufin-nexus which connects us to the bank.
    422 
    423 ### monitoring
    424 
    425 Provisions external node monitoring when `enable_monitoring` is true.
    426 
    427 ### pixel_borg
    428 
    429 Configures the host to hosting backups *from* pixel.
    430 
    431 ### reboot
    432 
    433 Reboots the system.
    434 
    435 ### stop_services
    436 
    437 Stops all Taler-related services. Useful for emergency stop and
    438 used as part of the reboot playbook.
    439 
    440 ### webserver
    441 
    442 Configures Nginx reverse proxy (main service, not individual subdomains).
    443 
    444 
    445 ## Checks
    446 
    447 ```
    448 $ ./contrib/test-fact-helpers.sh   # the /bin helpers that generate local facts
    449 ```
    450 
    451 Also run by the CI job in "contrib/ci/jobs/001-build".
    452 
    453 ## Deployment safety and recovery
    454 
    455 Normal deployment still upgrades system packages. It stops existing application
    456 services, targets, timers and sockets before package work, suppresses application
    457 starts through Debian's policy-rc.d mechanism, and starts applications
    458 only after all configuration and database migrations finish. Schedule deployment
    459 as downtime. During the system-wide upgrade, the temporary policy denies Taler
    460 service actions and delegates other actions to the existing administrator policy
    461 (or permits them if none existed). Application package tasks use policy_rc_d: 101.
    462 The original policy is restored even on failure. PostgreSQL and nginx are started
    463 as needed during provisioning.
    464 Cold exchange startup may spend several minutes generating denomination keys.
    465 post_deployment_http_retries defaults to 60 (with a five-second request timeout
    466 and one-second retry delay); systemd unit checks retain their shorter retry limit.
    467 
    468 A failed deployment leaves applications stopped without removing their boot
    469 enablement. Inspect the failed task, correct the problem, then rerun deploy.
    470 Do not reboot or manually start applications to bypass an unfinished migration.
    471 Optional services whose provisioning is skipped are restored if they were active
    472 when that deployment began. After a failed deployment, also inspect any optional
    473 services before returning the host to service.
    474 
    475 Setting dangerously_enable_devtesting to false revokes the managed login keys,
    476 sudo access and helper programs, terminates that account's sessions and disables
    477 the account. Its home-directory data is retained. Setting it true restores the
    478 managed account settings and access.
    479 
    480 The KYC attribute encryption key is preserved in a restricted secret include.
    481 Do not rotate this key as a substitute for moving it: changing it without a data
    482 migration would prevent decryption of existing attributes. Auditor authentication
    483 is stored in a root-only nginx include. Secret tasks suppress output and diffs,
    484 including when removing secrets from previously public configuration files.
    485 
    486 Backup and restore share borg_host and borg_repo inventory defaults, retaining
    487 ssh://borg@pixel.taler-systems.com/~/spec-backup for existing installations. Set
    488 borg_repo in host variables to use another already provisioned repository.
    489 Existing database_restore_borg_repository and database_restore_borg_host overrides
    490 remain supported. No archives are moved automatically.
    491 
    492 The backup script uses /root/.ssh/borg-config and leaves /root/.ssh/config intact.
    493 A lock protects the entire backup operation. Install the updated backup script
    494 between backup runs: an already running older script does not acquire this lock. If cron, backup or reboot
    495 encounters another backup, it exits nonzero (script status 75) without touching
    496 that run's snapshot. Retry after the existing backup finishes; a reboot playbook
    497 aborts before rebooting on this failure. Pruning and compaction are skipped after
    498 an unsuccessful or warning-producing archive creation.
    499 
    500 ## Exchange HTTP latency monitoring
    501 
    502 Sentol can opt an exchange into latency collection with
    503 `monitoring_nginx_latency_enabled: true` in its external-host inventory. Its
    504 exported public bundle then contains `monitoring_client.nginx_latency.enabled:
    505 true`. Redistribute the bundle and deploy this repository with `enable_monitoring`
    506 enabled. Missing or false capability values disable the feature; the capability
    507 must be a boolean and the exchange domain must be a safe hostname.
    508 
    509 Fluent Bit tails `/var/log/nginx/<exchange_domain>.tal` and aggregates nginx's
    510 `rt` field into `nginx_request_duration_seconds` histogram buckets, sum and count.
    511 The labels are `operation`, `method` and `status`. The checked-in
    512 `roles/monitoring/files/exchange-latency-routes.json` allowlist is shared by nginx
    513 GET selection and Fluent Bit classification; update it alongside exchange API
    514 route changes. Operation names omit identifiers; `/batch-deposit` is `deposit`.
    515 Unrecognized mutation routes are `other`. Malformed records increment
    516 `nginx_timing_log_parse_errors_total`; oversized lines are skipped so tailing can
    517 continue. Fluent Bit diagnostics report oversized lines separately.
    518 
    519 POST, PATCH and DELETE keep their existing coverage. The exchange's nginx
    520 virtual host additionally logs known API GETs, including ordinary requests to
    521 endpoints that optionally long-poll. Reserve status, deposit tracking, purse
    522 merge/deposit status, KYC check and KYC info GETs are included only when
    523 `timeout_ms` is absent or consists entirely of zero digits. Purse requests also
    524 check the documented `deposit_timeout_ms` argument conservatively. Calls with a
    525 positive timeout are excluded even when they return immediately.
    526 
    527 On these polling-capable routes, repeated timeout arguments, malformed values,
    528 and queries containing percent escapes or plus signs are excluded. This may omit
    529 unusual ordinary calls, but prevents encoded polling arguments entering latency
    530 charts. Query strings are inspected only inside nginx and are not added to timing
    531 logs. Ordinary `/keys` requests remain included when server trouble makes them
    532 slow. Unknown GET routes, metrics endpoints, static downloads, HEAD and OPTIONS
    533 are excluded. Other services' timing-log selection is unaffected.
    534 
    535 Generated metrics listen on `127.0.0.1:2021/metrics` and are exposed through the
    536 existing Sentol-only mTLS proxy at `/nginx/metrics`. Port 2021 must not conflict
    537 with the configured node exporter or metrics proxy. The usual collector-health
    538 endpoint on port 2020 and journal forwarding continue separately; timing records
    539 are consumed locally and never sent to VictoriaLogs.
    540 
    541 First enrollment starts at the end of the existing timing log. Later starts
    542 resume `/var/lib/fluent-bit/exchange-timing.db`; deployment must not remove this
    543 cursor. Rotation should rename the file and tell nginx to reopen its logs;
    544 Fluent Bit continues watching the old descriptor for 30 seconds. Rotation while
    545 the collector is stopped can lose observations: the input follows the active
    546 filename and does not replay rotated archives. A dedicated logrotate rule retains 14
    547 daily timing logs with delayed compression, matching Debian's standard nginx
    548 policy (whose `*.log` pattern does not cover `.tal`). This rule remains installed
    549 when latency metrics are disabled, since mutation timing logs are still written.
    550 
    551 Histogram state is in memory and resets on collector restart. Prometheus retains
    552 already scraped history, but can lose observations since its last scrape. Unread
    553 records appear when processed after recovery, which can temporarily distort
    554 rates. This is operational monitoring, not exact accounting. The Perses dashboard
    555 uses counter-aware five-minute queries and defers latency alert thresholds until
    556 a baseline is available. Disabling the capability removes the scrape route and
    557 collector input after redeployment, while preserving the cursor for later use.
    558 
    559 Diagnose collection locally with `curl http://127.0.0.1:2021/metrics` and
    560 `journalctl -u fluent-bit`; inspect the latency dashboard's scrape and parse-error
    561 panels on Sentol. Before deployment, configuration is checked with the pinned
    562 Fluent Bit binary's `--dry-run` mode and nginx's `-t` mode.
    563 
    564 ## Regression checks
    565 
    566 `python3 contrib/tests/test_deployment_recovery.py` uses the local
    567 `ansible-taler-test` image built by `./test` (or a Debian systemd image passed
    568 with `--image`). It runs the real setup failure/retry flow and recovery handlers
    569 against disposable service fixtures, checking lost notifications, unchanged
    570 retries, check mode, validation failures and disabled monitoring. It requires
    571 the `containers.podman` Ansible collection and runs without container networking
    572 or package downloads. The full `./test` runner includes this regression.
    573 
    574 `python3 contrib/tests/test_nginx_latency.py` checks the timing parser and bounded
    575 route classification (requires Python Jinja2 and Lua). The `tsys-infra/monitoring`
    576 Podman suite exercises this repository's real monitoring role, nginx GET
    577 selection, pinned Fluent Bit histograms, mTLS scraping, rotation, cursor recovery,
    578 and Perses queries across resets and idle windows.
    579 
    580 Run python3 contrib/tests/test_backup.py for isolated concurrency, failure,
    581 cleanup and archive round-trip checks (requires Borg and Python Jinja2).
    582 The extended ./test also exercises real systemd shutdown, package-start
    583 suppression, a pre-2.20 Ansible dependency bootstrap, secret permissions,
    584 devtesting revocation, preservation of unrelated configuration, failed deployment
    585 and recovery. All fixtures run in the disposable container, never an inventory
    586 production host. Monitoring regressions also cover Vault-encrypted onboarding
    587 bundles, preflight failures before application shutdown, authenticated metrics,
    588 JSON Lines delivery, certificate renewal, idempotence, and check mode.