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 ).
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).
Severity Meaning Exit code
FATAL Server will not boot with this config. 1
BREAK Behaviour 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
WEAK Currently works, but a stronger posture is available. 0 normally; 1 under --strict
INFO Positive observations. 0
The code field on every finding is stable — pin your CI rules
to those, not to headlines.
Every code the doctor can emit, grouped by area.
Severity Code Fires when
FATAL fatal-config-xroad-protocol-invalidxroad_protocol_version isn't "4.0" or "4.1" (audit-v1 M1).
INFO info-config-xroad-protocol-okProtocol version accepted.
INFO info-config-sourceWhich file was loaded (--config / XTR_CONFIG / ./xtr.yaml).
INFO info-config-defaultsNo config file found; using built-in defaults.
INFO info-limits-summarySnapshot of resource ceilings that will apply.
Severity Code Fires when
FATAL fatal-client-data-placeholder-member_codeclient_data.member_code still holds <placeholder> text.
FATAL fatal-client-data-placeholder-subsystem_codeSame for subsystem_code.
WEAK weak-client-data-emptyAll three client_data.* fields empty — envelope/header will have no identity.
Severity Code Fires when
FATAL fatal-keystore-env-missingsecurity_server: is set but its keystore_password_env variable is unset.
FATAL fatal-keystore-env-emptyEnv var is set but empty.
FATAL fatal-keystore-file-missingkeystore_path doesn't exist on disk.
INFO info-keystore-env-presentEnv var resolved.
Severity Code Fires when
WEAK weak-wsdl-allow-httpwsdl.allow_http_upstream: true (audit-v1 C1).
WEAK weak-wsdl-allowlist-emptywsdl_watch_dir set but wsdl.upstream_host_allowlist is [].
INFO info-wsdl-allowlist-pinnedNon-empty allowlist.
Severity Code Fires when
WEAK weak-error-expose-soap-fault-detailexpose_soap_fault_detail: true (audit-v1 H3).
Severity Code Fires when
FATAL fatal-rest-no-security-serverREST DSL(s) loaded but security_server: is unset. Every REST request would 500.
FATAL fatal-rest-ss-not-httpssecurity_server.url doesn't start with https:// (X-Road REST §4.7).
FATAL fatal-rest-target-fields-missingA REST DSL has empty target.member_class / member_code / subsystem_code / service_code.
WEAK weak-rest-identifier-charsetA REST DSL's target identifiers contain characters outside spec §4.8 (A-Za-z0-9'()+,-.=?).
INFO info-rest-lane-readyREST DSL(s) present and SS configured.
INFO info-rest-trust-ca-systemUsing system trust store for SS TLS — flag reminder to set trust_ca_path if the SS uses a private CA.
Severity Code Fires when
WEAK weak-limits-request-too-generousmax_request_bytes > 16 MiB.
WEAK weak-limits-response-too-generousmax_response_bytes > 128 MiB.
WEAK weak-limits-timeout-too-longrequest_timeout_secs > 300.
Severity Code Fires when
WEAK weak-paths-dsl-missingdsl_path doesn't exist.
WEAK weak-paths-wsdl-watch-missingwsdl_watch_dir set but path doesn't exist.
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.