morphit/ops/ansible/group_vars/all.yml
Morphit Team 7c1c3bb332
Some checks failed
morphit-ci / TypeScript typecheck (sweep all workspaces) (push) Has been cancelled
morphit-ci / apps/web svelte-check (svelte-kit sync + svelte-aware tsc) (push) Has been cancelled
morphit-ci / Integration tests (real Postgres 16) (push) Has been cancelled
morphit-ci / ansible-lint (playbook quality gate) (push) Has been cancelled
morphit-ci / Smoke suite (run-smokes.sh, triple-pulse) (push) Has been cancelled
morphit-release / Build + publish release tarball (push) Has been cancelled
v1.11.1 — registration self-heal + data-fs disk health + offline-install RPC softening + node-health unification (cp707-cp710)
2026-08-11 17:38:41 -07:00

725 lines
34 KiB
YAML

# Morphit Ansible — operator-editable values.
#
# EVERY value with `DUMMY-VALUE:` in its comment is a placeholder.
# Replace before first run. Use ansible-vault for secrets (passwords,
# keystore passphrase, alert email password); reference them as
# {{ vault_<name> }} below.
#
# Section references throughout point at docs/OPERATIONS.md in the
# morphit repo.
# ─── Identity ────────────────────────────────────────────────────
# DUMMY-VALUE: your instance's public domain. Used for TLS cert,
# BunkerWeb SERVER_NAME, /v1/instance origin, and the canonical URL
# in the frontend's <link rel="canonical">.
morphit_domain: morphit.example.com
# ─── Dynamic DNS (HOME nodes only) ───────────────────────────────
# A home box behind a router usually has a public IP that CHANGES. Turn this
# on to keep your domain pointed at it automatically (the `ddns` role installs
# a small updater + timer). A VPS has a STATIC IP — leave this false.
enable_ddns: false
# DUMMY-VALUE: your DNS provider's dynamic-DNS UPDATE URL, with `{ip}` where the
# current IP goes. Get a domain from ANY registrar; this URL comes from that
# registrar's dynamic-DNS docs. Required only when enable_ddns is true.
# Examples:
# Njalla: https://njal.la/update/?h=yourdomain&k=YOURKEY&a={ip}
# Namecheap: https://dynamicdns.park-your-domain.com/update?host=@&domain=yourdomain.com&password=YOURPW&ip={ip}
morphit_ddns_update_url: ""
# DUMMY-VALUE: the operator's BLURT account name (lowercase, 3-16
# chars, a-z 0-9 -). This is the account that signs operator-
# registration ops, posts releases, collects stranger fees, and
# attributes payments. See OPERATIONS.md §0.
morphit_operator_account: my-operator
# DUMMY-VALUE: short tag (≤16 chars) the indexer uses in
# operator_attribution_events. Often the same as
# morphit_operator_account, but can be shorter / branded.
morphit_operator_tag: myoperator
# Friendly title + one-line description for your instance, shown on the shared
# /instances directory (the wizard collects these; a remote/manual inventory can
# set them here). Empty title omits MORPHIT_INSTANCE_NAME so the indexer falls
# back to the operator account; empty tagline omits MORPHIT_INSTANCE_TAGLINE.
morphit_instance_name: ""
morphit_instance_tagline: ""
# The BLURT account that RECEIVES your listing fees. For a federation instance
# YOU earn these (your 90% share; the treasury's 10% is taken separately), so
# this defaults to your own operator account. Set it to the shared
# @morphit-fees treasury only if you are running the canonical morphit.io.
morphit_fee_recipient: "{{ morphit_operator_account }}"
# ─── Server users ────────────────────────────────────────────────
# Unprivileged service user that runs the morphit relay + indexer.
# Created by the `base` role. Don't change after first deploy
# without manual data migration.
morphit_service_user: morphit
morphit_service_group: morphit
morphit_service_home: /var/lib/morphit
# ─── PostgreSQL ──────────────────────────────────────────────────
# Bound to loopback only per §37.8. External access via PgBouncer
# is documented in §30 if you need it; this playbook does NOT
# install PgBouncer.
postgres_listen_addresses: "127.0.0.1"
postgres_port: 5432
# DB + user provisioned by the `postgres` role. There is ONE database: the
# relay uses the indexer DB (there is no separate relay database or user).
postgres_indexer_db: morphit_indexer
postgres_indexer_user: morphit_indexer
# DUMMY-VALUE: real passwords belong in group_vars/vault.yml
# (ansible-vault encrypted). Reference them here. For first-time
# scaffolding, replace these strings and re-run after creating the
# vault. See README §"Quick start" step 3.
postgres_indexer_password: "{{ vault_postgres_indexer_password | default('CHANGE-ME-INDEXER') }}"
# ─── Morphit source + build ──────────────────────────────────────
# Forgejo repo URL. See OPERATIONS.md §0 for the canonical source.
morphit_repo_url: https://git.agorise.net/agorise/morphit.git
# Git ref to deploy. Pin to a release tag for prod (e.g. "v1.0.0");
# `main` is suitable for staging.
morphit_repo_ref: main
# Local clone destination on the target host.
morphit_repo_path: /opt/morphit
# LOCAL install (set automatically by `morphit-ops install` on the box itself):
# when morphit_local_install is true the playbook allows running as root (no SSH
# session to lock out), and when morphit_local_source_path is set the morphit
# role deploys THAT extracted release instead of cloning git. Remote installs
# leave both at their defaults.
morphit_local_install: false
morphit_local_source_path: ""
# Where the repo's SOURCE files live *right now*, during the play. Roles that run
# BEFORE the morphit role reads a file out of the tree (e.g. ddns's updater
# script, postgres's init.sql) must use THIS, not morphit_repo_path: on a guided
# LOCAL install /opt/morphit is still empty until the morphit role copies the
# extracted release into it, so the source is the extraction dir
# (morphit_local_source_path). On a manual/remote install the repo already lives
# at morphit_repo_path from the start, so this falls back to it.
morphit_source_dir: "{{ morphit_local_source_path if ((morphit_local_source_path | default('')) | length > 0) else morphit_repo_path }}"
# True only during a self-contained OFFLINE (bundled) install — the vendor role
# flips this on when it finds the bundled apt repo. Roles that would otherwise
# fetch a third-party apt-repo key from the internet (e.g. bunkerweb's Docker
# repo) skip that step when this is set, installing from the bundled closure
# instead. Stays false on an ordinary online install.
morphit_offline_install: false
# ─── Node.js ─────────────────────────────────────────────────────
# The repo's package.json specifies the Node engine; verify before
# pinning here. As of Part 121, Node 22 LTS is canonical.
morphit_node_version: "22"
# ─── Relay environment (§18, §23, §38.7) ─────────────────────────
# DUMMY-VALUE: full path to the relay's Blurt keystore (active key,
# encrypted). Generated per RUN-A-MORPHIT-NODE.md; place on the
# target host BEFORE first run.
morphit_relay_keystore_path: /etc/morphit/relay.keystore
# (Part 122 cp5 F13) — `morphit_relay_keystore_passphrase` was
# previously defined here as a vault-with-default pattern. It is
# no longer needed: the relay's encrypted-envelope design unlocks
# the keystore via a systemd encrypted credential
# (`LoadCredentialEncrypted=` in morphit-relay.service) at boot.
# There is NO `MORPHIT_RELAY_PASSPHRASE` env var consumed by any
# code path; putting the passphrase in env would defeat the
# encrypted-envelope design. Removed to prevent operators from
# leaking their passphrase to a 0640 disk file out of misplaced
# template-completionism.
# Relay's HTTP bind. Behind the BunkerWeb + frontend-container
# topology (§32) the frontend nginx reaches the relay from the Docker
# bridge via host.docker.internal, so a 127.0.0.1-only bind would be
# UNREACHABLE (every proxied call 502s). Bind on all interfaces and
# let the firewall restrict it: the hardening role's UFW default-deny
# blocks the public from this port, and the bunkerweb role adds an
# allow for the bunkerweb_net CIDR only. Port 8080 is the morphit
# default (MORPHIT_RELAY_LISTEN_PORT).
morphit_relay_bind_host: 0.0.0.0
morphit_relay_bind_port: 8080
# Diamond-hardened squatter defense preset per §38.7. These are
# NOT dummy values — they're the canonical preset. Relax only
# after watching rejection logs (§38.2) and confirming you're
# blocking legitimate users.
morphit_relay_signup_enabled: true
morphit_relay_signup_daily_ceiling: 20
morphit_relay_create_spacing_minutes: 90
morphit_relay_altcha_trigger_count: 2
morphit_relay_altcha_maxnumber: 4000000
morphit_relay_highvalue_name_policy: strict
morphit_relay_highvalue_short_name_threshold: 5
morphit_relay_sequential_detector_enabled: true
morphit_relay_sequential_threshold: 1
morphit_relay_sequential_window_ms: 86400000
morphit_relay_sequential_min_prefix: 3
# Trusted-proxy IPs (§32 CRITICAL). Set to the IP/CIDR that
# BunkerWeb runs on. For the canonical BunkerWeb shipped with
# the morphit Ansible playbook (roles/bunkerweb/), the Docker
# network CIDR is PINNED at 172.20.0.0/16 (see
# roles/bunkerweb/templates/docker-compose.yml.j2:60). This
# default matches that pinned CIDR — DO NOT change unless you
# also change the bunkerweb role's docker-compose subnet, or you
# will silently break per-IP rate limiting (relay rejects
# X-Forwarded-For from BunkerWeb because it's on an untrusted
# CIDR, falls back to peer IP which is the BunkerWeb container's
# IP, and all users bucket into one rate-limit slot).
#
# Verify the live deploy with:
# docker network inspect bunkerweb_net --format \
# '{{ "{{" }}range .IPAM.Config{{ "}}" }}{{ "{{" }}.Subnet{{ "}}" }}{{ "{{" }}end{{ "}}" }}'
#
# Comma-separated for multiple proxies (e.g., CDN + BunkerWeb).
# CRITICAL: too narrow = X-Forwarded-For ignored = users share
# one rate-limit bucket. Too wide (e.g. 0.0.0.0/0) = any user can
# forge X-Forwarded-For = rate limits bypassed. Both are bad.
morphit_relay_trusted_proxy_ips: "172.20.0.0/16"
# ─── Indexer environment (§18, §23) ──────────────────────────────
# Indexer's HTTP bind. Same bridge-reachability requirement as the
# relay (§32) — the frontend container proxies to it via
# host.docker.internal, so bind on all interfaces. UFW default-deny
# blocks the public from this port and the bunkerweb role allows only
# the bunkerweb_net CIDR. Port 8081 is the morphit default
# (MORPHIT_INDEXER_LISTEN_PORT).
morphit_indexer_bind_host: 0.0.0.0
morphit_indexer_bind_port: 8081
# MCP server (§45) — same topology as the relay/indexer: the dockerized
# BunkerWeb frontend reaches it on the host across the Docker bridge, so
# it binds all interfaces and the bunkerweb role allows ONLY the
# bunkerweb_net CIDR to this port (UFW default-deny keeps the public
# out). Because the MCP is fail-closed on a non-private bind, the
# mcp.env template pairs this 0.0.0.0 bind with
# MORPHIT_MCP_ALLOW_PUBLIC_BIND=1 — the firewall is the protection,
# exactly as for the relay. Port 8124 is the morphit default. (A
# bare-metal operator who fronts the MCP with host nginx instead can set
# morphit_mcp_bind_host: 127.0.0.1 and drop the allow flag.)
morphit_mcp_bind_host: 0.0.0.0
morphit_mcp_bind_port: 8124
# Blurt RPC endpoints — see §22 for selection criteria. Defaults to
# the canonical six; the indexer and relay node-hop across them
# (latency-aware, fastest-first with failover). This list MUST stay in
# sync with DEFAULT_BLURT_RPC_ENDPOINTS in
# packages/operator-config/src/index.ts, and with the Blurt RPC hosts in
# outbound_allowed_destinations below. Override only if you run or trust
# a different set. Comma-separated.
morphit_indexer_blurt_rpc_endpoints: "https://rpc.drakernoise.com,https://blurtrpc.dagobert.uk,https://rpc.blurt.blog,https://rpc.beblurt.com,https://rpc.blurt.one,https://blurt-rpc.saboin.com"
# Asset registry: disable specific tradable assets if your instance
# specializes (§38, Memory #25). Comma-separated uppercase tickers.
# Empty string = no assets disabled (default-everything posture).
morphit_indexer_disabled_assets: ""
# Canonical payment-method KEYS this instance will NOT offer (cp208).
# Comma-separated, lowercase. Empty = offer every method (default).
# Most operators leave this empty or set "barter_goods" for a
# money-only instance. See OPERATIONS.md §"Payment-method configuration".
morphit_indexer_disabled_payment_methods: ""
# Attestation phase: 'launch' or 'steady'. See §20.
morphit_indexer_attestation_phase: launch
# ─── Backups (§31) ───────────────────────────────────────────────
# Daily backup timer fires at this systemd OnCalendar spec.
morphit_backup_schedule: "*-*-* 04:00:00"
# Docker-aware backup: set to the Postgres container name if this node
# runs its DB in a container (BunkerWeb / docker-compose). The daily
# backup then dumps THROUGH `docker exec <container> pg_dump …`. Leave
# EMPTY for a host-installed Postgres — the Ansible role installs
# Postgres on the host by default, so this is normally unset. (A manual
# /opt/morphit box with a containerized DB sets DB_CONTAINER in
# /etc/morphit/backup.env directly; `morphit-ops` auto-detects it there.)
morphit_db_container: ""
# Optional rsync/scp destination for off-host backup.
# CRITICAL: backups on the same disk as the DB are not backups.
# Leave EMPTY to keep backups local-only; the operator MUST
# replace this with a real destination before the off-host
# leg means anything. Placeholder values containing
# "REPLACE", "XXXXX", or "example.com" are detected by
# morphit-backup.sh and SKIPPED with a journald warning,
# rather than silently shipping plaintext to a bogus host.
# Examples:
# "backups@backup-host.your-org.tld:/morphit-backups/"
# "/mnt/external-volume/morphit-backups/" (separate physical disk)
# "s3://my-backups-bucket/morphit/" (with rclone configured)
morphit_backup_remote_destination: ""
# Path to the SSH key the backup script uses to push to the
# remote destination. Created by the `morphit` role. Empty
# when off-host push is not configured. When non-empty, the
# matching public key must be in the backup destination's
# authorized_keys.
morphit_backup_ssh_key_path: ""
# age recipient public key (https://age-encryption.org) for
# encrypting backups before write. Leave EMPTY to write
# plaintext .sql.gz files (acceptable when the host disk is
# LUKS-encrypted AND backups never leave the host). When
# set, every backup is encrypted with `age -r ...` before
# write; the matching PRIVATE key MUST live off this host
# (§37.12). Placeholder values containing "REPLACE" or
# "XXXXX" are detected and SKIPPED with a warning rather
# than silently leaking plaintext.
# Generate a fresh keypair with:
# age-keygen -o /path/to/safe/storage/morphit-backup.key
# The first line of the output file is the public key; put
# THAT here (starts with "age1...").
morphit_backup_age_recipient: ""
# How many days of local backups to retain. Remote retention is
# the operator's responsibility (rsync does not delete; older
# remote backups accumulate unless the operator prunes them).
morphit_backup_retention_days: 30
# ─── Operator alerting (§37.14) ──────────────────────────────────
# DUMMY-VALUE: where to send operator alerts (signup-velocity
# spike, stale price feed, low balance, etc.).
morphit_alert_email_to: "operator@example.com"
# DUMMY-VALUE: SMTP relay for outbound mail. Postfix is
# configured by the `morphit` role with this as the smarthost.
# Common choices: Mailgun, Postmark, AWS SES.
morphit_alert_smtp_host: "smtp.example.com"
morphit_alert_smtp_port: 587
morphit_alert_smtp_user: "alerts@example.com"
morphit_alert_smtp_password: "{{ vault_alert_smtp_password | default('CHANGE-ME-SMTP') }}"
# ─── TLS (§35) ───────────────────────────────────────────────────
enable_tls: true
# DUMMY-VALUE: where Let's Encrypt sends renewal notices + recovery
# email.
tls_acme_email: "operator@example.com"
# Let's Encrypt staging server for dry-runs. Set to true on first
# deploy to verify the cert flow without burning prod rate limits.
tls_use_staging: false
# Auto-register this instance on-chain the first time the box sees the
# internet (morphit-first-online). Opt-in via the installer wizard; the
# operator can always register by hand with `morphit-ops register` instead.
# Default OFF — registering is a deliberate, permanent on-chain act.
morphit_auto_register: false
# ─── Tor onion (privacy by default) ──────────────────────────────
# Every instance gets a basic v3 .onion (the wizard generates one and
# the site advertises it via MORPHIT_INSTANCE_TOR_ADDRESS — the footer
# pill + Onion-Location auto-redirect). This role makes Tor actually
# SERVE it. Set morphit_tor_key_src to the wizard's tor-hidden-service/
# directory (copied to the control node) so Tor serves the SAME address
# the site advertises; otherwise Tor self-generates a different one.
enable_tor: true
morphit_tor_hs_dir: /var/lib/tor/morphit
morphit_tor_local_host: 127.0.0.1
# cp695 — host loopback port the frontend container publishes so Tor + I2P
# hidden services reach the site fan-out (not the bare relay). Must match the
# port tor/i2pd target below + the docker-compose frontend ports mapping.
morphit_onion_frontend_port: 8090
morphit_tor_local_port: 8090 # cp695: the frontend fan-out (was 8080=relay-only, which 404d the site)
morphit_tor_key_src: ""
# Every instance also gets a basic .b32.i2p (the wizard generates one and
# the site advertises it via MORPHIT_INSTANCE_I2P_B32_ADDRESS → footer pill).
# This role makes i2pd SERVE it. Set morphit_i2pd_key_src to the wizard's
# i2p-tunnel/ directory (copied to the control node) so i2pd serves the SAME
# address the site advertises; otherwise i2pd self-generates a different one.
# An operable keyfile already on the server is PRESERVED (never overwritten).
enable_i2pd: true
morphit_i2pd_datadir: /var/lib/i2pd
morphit_i2pd_tunnels_conf: /etc/i2pd/tunnels.conf
morphit_i2pd_keyfile: morphit-web.dat
morphit_i2pd_local_host: 127.0.0.1
morphit_i2pd_local_port: 8090 # cp695: the frontend fan-out (was 8080=relay-only)
morphit_i2pd_key_src: ""
# ─── IPFS release hosting ────────────────────────────────────────
# Every Morphit instance runs a small Kubo node that pins THIS instance's
# current signed release, so Morphit's releases stay available even if every
# commercial pinning service (Pinata/Storacha/…) drops them. ON by default:
# decentralization is priority #2, and operators keep 90% of the BLURT listing
# fees — hosting the signed release is the quid pro quo. Low-footprint by
# design (lowpower Kubo profile, capped connections, ~12 MB pinned). To opt a
# host out, set enable_ipfs: false for that host in inventory.
enable_ipfs: true
morphit_kubo_version: "v0.42.0"
# Kubo tarball SHA-512 (hex) — HARD PIN, verified by default. Baked to the
# SHA-512 of the official kubo v0.42.0 linux-amd64 release asset (computed from
# github.com/ipfs/kubo/releases; dist.ipfs.tech publishes the byte-identical
# artifact, so the dist download is verified against exactly this). Bump this
# in lockstep with morphit_kubo_version. Set to "" to fall back to verifying
# against Kubo's published .sha512 instead.
morphit_kubo_sha512: "054c38a0cf66f7d738e25085ad62cb3a42d03d4bac329b7dd25c1d71cf18e1ce87d55b1d1b705b04c65210dca9109973579e0eb1cd72f6341ecb3311d840d156"
morphit_ipfs_pin_on_calendar: hourly
# ─── BunkerWeb (§32) ─────────────────────────────────────────────
enable_bunkerweb: true
# BunkerWeb Docker image tag. Verify against the BunkerWeb
# docs at deploy time — env-var names sometimes change between
# major versions.
# https://docs.bunkerweb.io
bunkerweb_image: bunkerity/bunkerweb:1.5.10
bunkerweb_scheduler_image: bunkerity/bunkerweb-scheduler:1.5.10
# OWASP CRS paranoia level. 1 = lenient, 4 = paranoid. 3 is the
# sweet spot for a public-facing API — strict enough to catch
# attacks, lenient enough to not break legitimate Morphit traffic.
bunkerweb_crs_paranoia: 3
# Block hosting-provider ASNs that are over-represented in
# squatter traffic (§38.6 item c). Verify against your own
# rejection logs before enabling — real users on these networks
# do exist (rare, but they do).
bunkerweb_block_asns:
- "AS14061" # DigitalOcean
- "AS24940" # Hetzner
- "AS16276" # OVH
# Add more as your rejection logs reveal patterns.
# Country-block list for §38.6 item b. Empty by default; populate
# only under active attack. ISO-3166-1 alpha-2 codes.
bunkerweb_block_countries: []
# ─── UFW (§34) ───────────────────────────────────────────────────
# Inbound ports to allow. SSH + HTTP (for ACME) + HTTPS.
# Everything else gets denied.
ufw_allowed_inbound_ports:
- "22" # SSH
- "80" # HTTP (ACME challenge + redirect to HTTPS)
- "443" # HTTPS
# ─── fail2ban (§34) ──────────────────────────────────────────────
# Bantime in seconds. 3600 = 1 hour for first offense. Repeat
# offenders get escalating bans up to bantime_max.
fail2ban_bantime: 3600
fail2ban_bantime_max: 604800 # 7 days
fail2ban_findtime: 600 # 10 min window
fail2ban_maxretry: 4
# ─── auditd (§37.6) ──────────────────────────────────────────────
# Default — full coverage of CIS-recommended audit rules. See
# roles/hardening/templates/audit.rules.j2 for the policy file.
# Override here only if you have specific compliance requirements.
audit_log_max_size_mb: 1024
audit_log_max_files: 5
# ─── Outbound network policy (§37.13) ────────────────────────────
# Hosts the morphit services + apt are allowed to reach. Anything
# else outbound is dropped by UFW. Add to this list if your
# backup destination, RPC endpoints, or SMTP host are not already
# resolvable from the items above.
outbound_allowed_destinations:
# apt repos
- archive.ubuntu.com
- security.ubuntu.com
- download.docker.com # BunkerWeb deploy
# Blurt RPC — the canonical six (keep in sync with
# morphit_indexer_blurt_rpc_endpoints / DEFAULT_BLURT_RPC_ENDPOINTS)
- rpc.drakernoise.com
- blurtrpc.dagobert.uk
- rpc.blurt.blog
- rpc.beblurt.com
- rpc.blurt.one
- blurt-rpc.saboin.com
# ACME
- acme-v02.api.letsencrypt.org
# Federation probes — must be open since they target arbitrary
# peer instances by design. The probe-layer SSRF defenses in
# apps/indexer/src/indexer/federationProbe.ts handle the
# risk (HTTPS-only, private-network deny list, 256KB cap,
# manual-redirect, AbortController timeout).
# matrix-bot prerequisites — only relevant if enable_matrix_bot
# is true. Comment out if you're not using Matrix.
- nodejs.org # better-sqlite3 native build headers
- registry.npmjs.org # npm package fetch (matrix-bot-sdk etc.)
# trivy CVE DB hosts — only relevant if enable_trivy_monitor.
- ghcr.io # trivy CVE DB image
- aquasecurity.github.io # trivy apt-repo signing key
- mirror.gcr.io # trivy CVE DB fallback
# ─── Operator-alerts via Matrix sidecar (cp9, §16) ──────────────
#
# Set enable_matrix_bot: true and populate matrix_bot_* vars in
# vault.yml to deploy the operator-alert sidecar. Refer to
# OPERATIONS.md §16 "Canonical Matrix routing — apps/matrix-bot"
# for the full setup procedure including how to mint the bot
# account access token.
#
# CRITICAL: matrix_bot_alert_mxid uses @user:server (PRIVATE
# MXID). Never set it to a #room:server alias — that would
# leak operator alerts to a public room. The bot config
# validator refuses #-prefixed values with privacy framing in
# the error, but defense-in-depth: don't tempt fate.
enable_matrix_bot: false
# matrix_bot_homeserver: "https://matrix.org"
# matrix_bot_access_token: "{{ vault_matrix_bot_access_token }}"
# matrix_bot_alert_mxid: "@you:matrix.org,@your-backup:matrix.org"
# matrix_bot_digest_time: "09:00" # daily digest UTC time
# matrix_bot_dry_run: false # set true for staging
# ─── MCP server (Model Context Protocol — AI-agent surface, §45) ─
#
# The morphit-mcp service exposes this instance's slice of the
# federated orderbook to AI agents (Claude / ChatGPT / Grok /
# Cursor / Cline / local LLMs). Read-only + non-custodial: it
# holds NO keys and signs NO trades — it hands the user off to the
# web UI for the actual key-signing, so the zero-KYC + non-custodial
# invariant is preserved. ON BY DEFAULT.
#
# It runs ISOLATED on purpose: its own low-privilege `morphit-mcp`
# user + its own directory (/opt/morphit-mcp) with ReadOnlyPaths
# locked, so the MCP process CANNOT read the main install's DB
# password or relay keys. The role deploys it as a self-contained
# tree (the two pure @morphit/* workspace deps are vendored).
#
# Set to false to skip the MCP deploy + service entirely. At
# runtime you can also flip it off without re-running Ansible:
# `sudo morphit-ops mcp` (or the menu → Check & operate → MCP server).
morphit_mcp_enabled: true
# Which Morphit instance the MCP server surfaces to AI agents. Defaults
# to THIS node's own public origin so a self-hosted MCP serves the
# operator's own orderbook (the server's built-in fallback is the public
# morphit.io instance, which a self-hoster almost never wants).
morphit_mcp_instance_url: "https://{{ morphit_domain }}"
# Advertise this node's MCP endpoint in /v1/instance.mcp_url so other AI
# agents discover it federation-wide — the live `<origin>/mcp` route the
# BunkerWeb frontend now proxies. Defaults true to match the init
# wizard's default-Yes (and the brag-list claim); set false to run the
# MCP without announcing it to the federation directory. Consumed by
# the INDEXER (it builds mcp_url), so it lives in indexer.env.
morphit_mcp_advertise: true
# ─── Web Push (VAPID — relay feature, §46) ──────────────────────
#
# Web Push lets users opt into browser/OS notifications for trade
# events. It is a RELAY feature gated on a VAPID keypair (RFC 8292):
# when the keys are present the relay's push-sender worker runs; when
# absent every push endpoint returns 503. ON BY DEFAULT — the role
# generates the keypair ONCE into /etc/morphit/relay-vapid.env (it is
# never regenerated on re-converge: rotating the public key would
# invalidate every existing subscription), and the relay unit sources
# that file if present.
#
# The VAPID subject MUST be a mailto: or https:// URL identifying the
# operator to the push services (they use it to contact you if your
# pushes misbehave). Defaults to this instance's public origin.
# Override with e.g. "mailto:ops@example.com".
morphit_enable_web_push: true
morphit_vapid_subject: "https://{{ morphit_domain }}"
# ─── Health disk figure (cp708) ─────────────────────────────────
#
# The /v1/health `system.disk_*` figure (and the `morphit-ops` health
# view) measures the filesystem that holds the node's DATA — the
# unbounded grower (Postgres / the chain index) — not always `/`. The
# indexer.env template defaults this to the Postgres data directory's
# parent, so statfs resolves it to whichever mount holds the DB. On a
# single-volume box that's the same as `/`; on a split-volume node it
# tracks the data mount. Override only if your data lives elsewhere.
# Must be an ABSOLUTE path; a bad value safely falls back to `/`.
# morphit_health_disk_path: "/var/lib/postgresql"
# ─── Host-resource monitor sidecar (cp10, §16) ──────────────────
#
# Disk / memory / swap / CPU thresholds. All optional — the
# script has sensible defaults for a 1-4 vCPU / 2-8 GB VPS.
enable_host_monitor: false
# host_monitor_disk_paths: "/ /var" # space-separated mounts
# host_monitor_disk_critical: 95
# host_monitor_disk_warn: 85
# host_monitor_disk_info: 70
# host_monitor_mem_critical: 95
# host_monitor_mem_warn: 85
# host_monitor_mem_info: 70
# host_monitor_swap_critical: 75
# host_monitor_swap_warn: 50
# host_monitor_swap_info: 25
# host_monitor_swap_thrash_critical: 1000 # pages/sec
# host_monitor_swap_thrash_warn: 100
# host_monitor_cpu_critical: 5.0 # loadavg/cores
# host_monitor_cpu_warn: 3.0
# host_monitor_cpu_info: 1.5
# ─── Disk SMART monitor sidecar (cp11, §16) ─────────────────────
#
# Polls smartctl every 6h on all detected block devices.
# Installs smartmontools. Useful on bare-metal hosts; less
# useful on VPS providers that virtualize disks.
enable_smartctl_monitor: false
# smartctl_temp_critical: 60 # °C
# smartctl_temp_warn: 50
# ─── fail2ban monitor sidecar (cp11, §16) ───────────────────────
#
# Polls fail2ban-client every 5 min — alerts on ban-count
# spikes (potential attack) and on the fail2ban daemon being
# unreachable (which means brute-force is NOT being blocked).
# Requires fail2ban itself, which the hardening role installs.
enable_fail2ban_monitor: false
# fail2ban_ban_critical: 50
# fail2ban_ban_warn: 15
# fail2ban_per_jail: # per-jail overrides
# sshd:
# critical: 100
# warn: 30
# postfix-sasl:
# critical: 20
# warn: 5
# ─── mdadm (Linux software RAID) monitor sidecar (cp11, §16) ────
#
# Polls /proc/mdstat every 15 min — alerts on array_failed /
# array_degraded, INFO on array_resyncing (informational).
# Safe to enable defensively even on non-RAID hosts: the script
# exits silently if /proc/mdstat has no md* entries.
enable_mdadm_monitor: false
# ─── Kernel-log monitor sidecar (cp12, §16) ─────────────────────
#
# Scans dmesg every 5 min for OOM-killer activations, kernel
# oopses, kernel panics, hardware errors (MCE/EDAC/ATA), and
# segfaults. Catches events the host-monitor sidecar CAN'T —
# host-monitor sees memory pressure building; dmesg_monitor sees
# the consequences when it broke.
enable_dmesg_monitor: false
# (no tuning vars — dmesg events are categorical, not threshold-based)
# ─── Docker image CVE rescan sidecar (cp12, §16) ────────────────
#
# Daily trivy scan of running Docker images for CRITICAL + HIGH
# CVEs. Most relevant for operators using the BunkerWeb deploy
# path (§32) since BunkerWeb's Docker image accumulates CVEs
# between releases. Useless on bare-metal-only deploys.
#
# trivy needs outbound HTTPS to ghcr.io / mirror.gcr.io to pull
# its CVE DB. Make sure those are in outbound_allowed_destinations.
enable_trivy_monitor: false
# trivy_critical_threshold: 1 # any CRITICAL CVE triggers alert
# trivy_high_threshold: 5
# trivy_scan_timeout: 300 # seconds per image
# ─── Postfix queue monitor sidecar (cp12, §16) ──────────────────
#
# Watches postfix mail queue depth + oldest message age.
# Critical observability gap: if email alerting silently fails
# (smarthost credentials rotated, TLS bumped, network down),
# the operator wouldn't know — the absence of alerts becomes
# its own alert.
#
# Useful only if you actually run postfix as your alerting
# smarthost (per the §37.14 alerting role).
enable_postfix_monitor: false
# postfix_queue_critical: 100 # queued messages
# postfix_queue_warn: 25
# postfix_age_critical_min: 120 # oldest-message age, minutes
# postfix_age_warn_min: 30
# ─── TLS certbot monitor sidecar (cp13, §16) ────────────────────
#
# Daily check of TLS cert expiry + a renewal-stall detector
# (cert expiring AND no successful renewal in N days). Most
# monitoring stacks miss the "renewal silently broke months ago"
# pattern; this catches it.
enable_certbot_monitor: false
# certbot_expiry_critical_days: 7
# certbot_expiry_warn_days: 30
# certbot_renewal_stall_days: 14
# ─── apt pending security updates sidecar (cp13, §16) ───────────
#
# Daily count of pending security updates. Surfaces what the
# motd shows but operators stop reading after the first month.
# Debian/Ubuntu only.
enable_apt_monitor: false
# apt_security_critical: 10 # pending security updates
# apt_security_warn: 1
# ─── Docker Compose health monitor sidecar (cp13, §16) ──────────
#
# Watches Docker Compose service health + restart loops. Most
# useful with BunkerWeb compose stack. Useless on bare-metal-
# only deploys (sidecar exits cleanly with INFO event).
enable_compose_monitor: false
# compose_restart_threshold: 5
# compose_projects: /opt/morphit/ops/bunkerweb
# ─── systemd unit-health monitor sidecar (cp14, §16) ────────────
#
# Watches morphit-* units (plus any in MORPHIT_SYSTEMD_WATCH) for
# "failed" state and high restart counts. Catches what journalctl-
# based alerting CAN'T: a unit that fails to start emits no journal
# output for the bot to route.
enable_systemd_monitor: false
# systemd_monitor_restart_threshold: 10
# systemd_monitor_extra_watch: "postgres@16-main.service docker.service"
# ─── journald disk-usage monitor sidecar (cp14, §16) ────────────
#
# Daily check of journal disk usage + time span covered. Catches
# the "journal silently grew for 6 months until disk full" pattern
# operators usually only find when it's too late.
enable_journald_monitor: false
# journald_size_critical_mb: 4096
# journald_size_warn_mb: 1024
# journald_rotation_stale_days: 90
# journald_rotation_stale_min_mb: 500