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
725 lines
34 KiB
YAML
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
|