Doctor & migration

XTR ships an in-image config validator: xtr-on-rust doctor. Run it against your xtr.yaml before every deploy — it flags what will break at boot, what changed vs the previous minor version, and where a stronger security posture is available.

The full migration guide lives in MIGRATION.md at the repo root (mirrored in this book under reference/migration).

Recipe

docker run --rm \
  -v "$(pwd)/xtr.yaml:/app/xtr.yaml:ro" \
  -v "$(pwd)/DSL:/app/DSL:ro" \
  turnerrainer/xtr:rc doctor --strict

Mount the DSL tree too — several REST-lane rules only fire when the doctor can see the loaded DSL files (e.g. it can only warn about a missing Security Server if REST DSLs are present).

Findings model

SeverityMeaningExit code
FATALServer will not boot with this config.1
BREAKBehaviour changed vs last minor and your config is on the losing side. Set the named recovery flag if you need bit-for-bit equivalence.1
WEAKCurrently works, but a stronger posture is available.0 normally; 1 under --strict
INFOPositive observations.0

The code field on every finding is stable — pin your CI rules to those, not to headlines.

Rule catalogue

Every code the doctor can emit, grouped by area.

Startup validation (always checked)

SeverityCodeFires when
FATALfatal-config-xroad-protocol-invalidxroad_protocol_version isn't "4.0" or "4.1" (audit-v1 M1).
INFOinfo-config-xroad-protocol-okProtocol version accepted.
INFOinfo-config-sourceWhich file was loaded (--config / XTR_CONFIG / ./xtr.yaml).
INFOinfo-config-defaultsNo config file found; using built-in defaults.
INFOinfo-limits-summarySnapshot of resource ceilings that will apply.

X-Road identity (SOAP + REST)

SeverityCodeFires when
FATALfatal-client-data-placeholder-member_codeclient_data.member_code still holds <placeholder> text.
FATALfatal-client-data-placeholder-subsystem_codeSame for subsystem_code.
WEAKweak-client-data-emptyAll three client_data.* fields empty — envelope/header will have no identity.

Security Server + mTLS (used by REST + Security-Server-routed SOAP)

SeverityCodeFires when
FATALfatal-keystore-env-missingsecurity_server: is set but its keystore_password_env variable is unset.
FATALfatal-keystore-env-emptyEnv var is set but empty.
FATALfatal-keystore-file-missingkeystore_path doesn't exist on disk.
INFOinfo-keystore-env-presentEnv var resolved.

WSDL folder-drop (SOAP lane)

SeverityCodeFires when
WEAKweak-wsdl-allow-httpwsdl.allow_http_upstream: true (audit-v1 C1).
WEAKweak-wsdl-allowlist-emptywsdl_watch_dir set but wsdl.upstream_host_allowlist is [].
INFOinfo-wsdl-allowlist-pinnedNon-empty allowlist.

SOAP fault exposure

SeverityCodeFires when
WEAKweak-error-expose-soap-fault-detailexpose_soap_fault_detail: true (audit-v1 H3).

REST lane (issue #5)

SeverityCodeFires when
FATALfatal-rest-no-security-serverREST DSL(s) loaded but security_server: is unset. Every REST request would 500.
FATALfatal-rest-ss-not-httpssecurity_server.url doesn't start with https:// (X-Road REST §4.7).
FATALfatal-rest-target-fields-missingA REST DSL has empty target.member_class / member_code / subsystem_code / service_code.
WEAKweak-rest-identifier-charsetA REST DSL's target identifiers contain characters outside spec §4.8 (A-Za-z0-9'()+,-.=?).
INFOinfo-rest-lane-readyREST DSL(s) present and SS configured.
INFOinfo-rest-trust-ca-systemUsing system trust store for SS TLS — flag reminder to set trust_ca_path if the SS uses a private CA.

Resource limits

SeverityCodeFires when
WEAKweak-limits-request-too-generousmax_request_bytes > 16 MiB.
WEAKweak-limits-response-too-generousmax_response_bytes > 128 MiB.
WEAKweak-limits-timeout-too-longrequest_timeout_secs > 300.

Path checks

SeverityCodeFires when
WEAKweak-paths-dsl-missingdsl_path doesn't exist.
WEAKweak-paths-wsdl-watch-missingwsdl_watch_dir set but path doesn't exist.

Machine-readable output

xtr-on-rust doctor --format json | jq '.[] | select(.severity == "FATAL")'

Stable schema per finding:

{
  "severity": "FATAL",
  "code":     "fatal-rest-no-security-server",
  "field":    "security_server",
  "headline": "3 REST DSL(s) loaded but security_server is unset",
  "rationale": "...",
  "recovery":  "..."
}
# .github/workflows/xtr-config-gate.yml
name: XTR config gate
on:
  pull_request:
    paths: [xtr.yaml, wsdl/**, DSL/**]
jobs:
  doctor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - run: |
          docker run --rm \
            -v "$PWD/xtr.yaml:/app/xtr.yaml:ro" \
            -v "$PWD/wsdl:/app/wsdl:ro" \
            -v "$PWD/DSL:/app/DSL:ro" \
            turnerrainer/xtr:rc doctor --strict --format json \
          | tee doctor.json
      - run: |
          fatal=$(jq '[.[] | select(.severity=="FATAL")] | length' doctor.json)
          [ "$fatal" -eq 0 ] || { echo "::error::$fatal FATAL finding(s)"; exit 1; }

See the migration reference for the exact breaking changes across minor versions with their recovery flags.