Command Line Reference¶
Rift provides Mountebank-compatible CLI options for easy migration.
The command is
rift. Docker, Homebrew, the release archives and the install script all put the server on yourPATHunder that name. The one exception iscargo install rift-http-proxy, which names the binary after the crate — substituterift-http-proxyforriftin every example below if you installed that way.
Basic Usage¶
# Start the server
rift
# With configuration file
rift --configfile imposters.json
# With custom port
rift --port 3525
Imposter Sources¶
--imposters loads imposters from one or more URIs instead of a single local path. Sources are
merged in the order given, and POST /admin/reload re-fetches all of them.
# A local file (these three are identical)
rift --configfile mocks.json
rift --imposters file:mocks.json
rift --imposters mocks.json
# A document served over HTTP
rift --imposters https://config.example.com/imposters.json
# Several sources merged into one running set
rift --imposters file:base.json,https://config.example.com/team-overrides.json
--configfile <p> is sugar for --imposters file:<p> and behaves identically; passing both is an
error. RIFT_IMPOSTERS is the environment-variable spelling.
Built-in schemes¶
| Scheme | Form | Version token |
|---|---|---|
file: |
file:<path>, or a bare path |
none — always re-read |
http: / https: |
a full URL | the response ETag |
Embedders register their own schemes (this is how the Rift Cluster git+https:, s3: and
registry: providers attach), so the table above is the built-in set, not the whole set.
How a scheme is recognised¶
Both scheme://rest and the shorter scheme:rest dispatch on their scheme, so s3://bucket/key
and s3:bucket/key reach the same provider. The :// form is checked first, so a compound scheme
such as git+https://… resolves to git+https rather than to git.
A URI without :// is read as scheme:rest only when what precedes the first colon is actually
scheme-shaped — RFC 3986 §3.1
(ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )) and longer than one character. Everything else
is a path, and paths are file::
rift --imposters s3:bucket/key # scheme `s3`
rift --imposters C:\mocks.json # a path — a drive letter is one character, so not a scheme
rift --imposters ./a:b.json # a path — does not start with a letter
rift --imposters my_source:key # a path — `_` is not an RFC 3986 scheme character
The one-character rule is a deliberate deviation from the RFC: single-letter schemes are legal there, but keeping Windows drive-letter paths working is worth more than reserving them.
A path whose leading segment is scheme-shaped (weird:path.json, or a Unix filename ending in a
colon) is taken as a scheme and fails at startup with no imposter source is registered for the
'weird:' scheme, listing the schemes that are. Spell such a path file:weird:path.json — the
file: prefix is stripped verbatim and the rest is opened as-is.
Merging¶
Every source contributes its imposters to one set. A port declared by two sources is a
startup error naming both — the alternative, letting the last source win, silently drops an
imposter the operator asked for. The optional intercept and routes blocks follow the same
rule: at most one source may declare each.
Reload and ETag¶
POST /admin/reload re-fetches every source. An https: source sends If-None-Match with the
ETag it last saw; a 304 Not Modified is served from cache — imposters, routes and intercept
block alike — without re-parsing, and when every
source reports no change the reload returns without touching the running imposters at all:
{"message": "No source changed; imposters left as they are",
"created": [], "replaced": [], "stubPatched": [], "toggled": [], "deleted": []}
toggled lists ports whose only change was the enabled flag, applied in place.
When the config file has an intercept block, the reply of a reload that applies changes also reports
"intercept": {"rulesSeeded": n, "rulesRuntime": m}. See
Hot reload.
When something did change, the existing incremental apply runs: unchanged imposters keep their recorded requests, scenario state and response cyclers; only changed ports are patched or replaced.
What a remote document may not do¶
A document fetched over the network is not written by someone who already has access to the
machine running Rift, so two things a local --configfile may do are refused for https:
sources:
include,stringifyandinject, in every spelling (<% include 'path' %>,<%- include('path') %>,<%- stringify(filename, 'path') %>, ...) — each reads a local file. Refused with an error naming the tag._rift.scriptfile:references — refused, as they are for admin-API-created imposters without--scripts-dir.require()outside--scripts-dir— a script in a fetched imposter loads modules only from under--scripts-dir, and none without it, as an admin-API imposter's does (issue #1611).lookupCSV files outside--scripts-dir— a fetched imposter'slookupreads CSV files only from under--scripts-dir, and none without it, as an admin-API imposter's does (issue #1617).
Any other tag the preprocessor does not evaluate is also refused, as it is for a local file, and
a fetched document has no --no-parse.
<%= process.env.VAR %> is substituted for remote documents: environment is deployment
configuration the operator supplied to their own process. Note the consequence — a remote source
you do not control can read your process environment into an imposter response body. Point
--imposters only at hosts you trust as much as the config file they replace.
Limits¶
| Limit | Value |
|---|---|
| Response body | 10 MB (enforced while reading, not from Content-Length) |
| Request timeout | 30 s |
| Redirects | followed only while the target stays http/https; at most 10 hops |
Adding a scheme¶
Embedders register their own schemes on the builder:
A source declares the schemes it claims via ImposterSource::schemes. Claiming a scheme that is
already registered — including a built-in — is a startup error rather than a silent override.
Providers return bytes; parsing goes through the same loader the built-ins use, so no scheme can
grow its own dialect of the config format.
CLI Options¶
rift [OPTIONS] [COMMAND]
Server options:
-p, --port <PORT> Admin API port [default: 2525]
--host <HOST> Address to bind the admin API and host-less imposters to (IPv4, IPv6 bare `::1` or bracketed `[::1]`, or a DNS name, resolved at startup) [default: every interface, IPv6 and IPv4]
--configfile <FILE> Load imposters from a JSON/YAML file on startup (sugar for --imposters file:<FILE>)
--imposters <URI[,URI...]> Load imposters from one or more source URIs: file:<path>, a bare path, or https://… (see Imposter Sources below)
--datadir <DIR> Directory for persistent imposter storage
--scripts-dir <DIR> Root directory for admin-API `file:`/`ref:` script resolution and for `require()` and `lookup` CSV files in admin-API, `--datadir` and `https:` imposters; paths that escape it are rejected (unset ⇒ all are refused)
-i, --allow-injection Enable JavaScript injection in responses (alias: --allowInjection)
-l, --local-only Only accept requests from localhost: the admin API, /metrics and --intercept-port listen on `127.0.0.1` and `::1`, and they, every imposter port and the front door refuse non-loopback clients (alias: --localOnly)
--require-admin-auth Refuse to start when the admin API would bind a non-loopback address with no --api-key (default: warn)
--loglevel <LEVEL> Log level: trace, debug, info, warn, error (an unrecognised value is refused) [default: info]
--runtime <MODE> Runtime topology: work-stealing (default) or per-core[=N] (RFC-712; experimental, Linux-first — macOS falls back with a warning, Windows rejects it)
--runtime-affinity Pin per-core worker threads to CPU cores (with --runtime per-core; effective on Linux)
--metrics-port <PORT> Prometheus metrics port [default: 9090]
--front-door <ADDR> Serve every imposter from one address, routed by host/path/header (see Features -> Front Door)
-w, --ip-whitelist <IPS> Comma-separated allowed IPs (accepted for Mountebank compatibility; NOT enforced) (alias: --ipWhitelist)
-m, --mock Accepted for Mountebank compatibility; no effect — set `recordRequests: true` per imposter
-d, --debug Enable debug mode (same as RIFT_DEBUG=1; also sets the log level to debug unless RUST_LOG is set; does not add Mountebank's per-stub `matches`)
--nologfile Do not write the --log file (stdout only)
--log <FILE> Also write logs to this file (off unless set) (alias: --logfile)
--pidfile <FILE> Write the server's PID here (off unless set; stop/restart read ./rift.pid when omitted)
-o, --origin <ORIGIN> Accepted for Mountebank compatibility; NOT implemented — the admin API sends no CORS headers
--api-key <TOKEN> Require this token in the Authorization header (or Mountebank's x-api-key) for all admin API requests; Authorization wins when both are sent (alias: --apikey)
--rcfile <FILE> RC file with default flag values (a subset: port/host/loglevel/allowInjection/localOnly/requireAdminAuth/apiKey/datadir/configfile/noParse/mock/debug); a path that does not exist warns and starts; one that exists but cannot be read or applied aborts startup
--default-tls-cert <FILE> Default TLS certificate (PEM) for HTTPS imposters without their own
--default-tls-key <FILE> Default TLS private key (PEM), paired with --default-tls-cert
--no-self-signed-tls Disable the self-signed fallback; an HTTPS imposter with no cert is an error
--upstream-ca-file <FILE> Extra CA certificate(s) (PEM) trusted for outbound TLS (proxy stubs, --configfile URLs); appended to the OS trust store
--upstream-tls-skip-verify Accept any certificate on outbound TLS (development only; prefer --upstream-ca-file)
--max-body-size <BYTES> Largest request, admin or proxied-response body accepted; over it is a 413 (default 536870912, 512 MiB)
--body-memory-budget <BYTES> Memory shared by every body buffered at once; used up, a request gets 503 + Retry-After (default 1073741824, 1 GiB; 0 = unbounded)
--journal-max-bytes <BYTES> Bytes of recorded requests kept per imposter, oldest evicted first (default 805306368, 768 MiB)
--intercept-port <PORT> Start the TLS-MITM intercept/redirect proxy on this port (epic #394); off when unset
--intercept-auth <USER:PASS> Require Proxy-Authorization: Basic on every CONNECT to the intercept proxy; open when unset
--intercept-ca-cert <FILE> PEM CA certificate for interception (with --intercept-ca-key); a CA is generated if omitted
--intercept-ca-key <FILE> PEM CA private key for interception (required with --intercept-ca-cert)
--intercept-ca-cert-pem <PEM> Inline PEM CA certificate for interception (with --intercept-ca-key-pem); mutually exclusive with file paths
--intercept-ca-key-pem <PEM> Inline PEM CA private key for interception (required with --intercept-ca-cert-pem)
--no-parse Disable EJS preprocessing of --configfile/file: sources; use it when a document contains a literal `<%` (alias: --noParse)
--formatter <NAME> Custom config formatter module (no-op; Rift auto-detects JSON/YAML)
--protofile <FILE> Custom protocol definitions file (no-op; custom protocols unsupported)
-h, --help Print help
-V, --version Print version
--no-parse disables EJS preprocessing of --configfile (<% include %> / <%= process.env.X %>
expansion), which is otherwise applied on load. A tag the preprocessor does not evaluate fails the
load, so use --no-parse when a document contains a literal <%. A <%= process.env.VAR %> whose
variable is unset (and has no || 'default') renders empty and logs a WARN naming the variable
and where the tag is; if the rendered document then fails to parse, the error names it too.
Mountebank's spellings --localOnly, --logfile, --ipWhitelist and --apikey are aliases of
--local-only, --log, --ip-whitelist and --api-key, alongside --allowInjection and --noParse (and save's
--removeProxies, of --remove-proxies). Unlike mb, --logfile has no default: Rift writes no
log file unless one is named.
Mountebank's short flags -p, -d, -i, -l, -m, -w and -o are accepted as --port,
--debug, --allow-injection, --local-only, --mock, --ip-whitelist and --origin, on
either side of a subcommand like the long flags, and save -r is save --remove-proxies. They
mean what they mean in mb (src/cli/cli.js): -r is removeProxies and -o is origin, not
--rcfile and --pidfile as issue #1586's triage first proposed. -h is --help, not
Mountebank's --host: spell --host out.
--formatter, --protofile, --ip-whitelist, --origin and --mock are accepted for Mountebank
command-line compatibility but have no effect in Rift. Each logs a warning when given. --origin
sets the admin API's CORS origin in Mountebank; Rift's admin API sends no CORS headers (an
imposter's own allowCORS is unrelated). Mountebank deprecated --mock; set recordRequests: true
on each imposter instead.
--ip-whitelist does not filter anything¶
--ip-whitelist has never applied IP filtering in any release of Rift. It parses, and is
otherwise ignored; passing it now logs a warning saying so. Earlier versions of this page advertised
it as a way to "restrict access", including a CIDR example — that syntax was never implemented
either. If you were relying on it, you had no filtering.
This is deliberate rather than a gap waiting to be filled. Network-level access control belongs to
the network, which sees the real peer: behind a proxy, load balancer or container NAT this process
sees the hop, not the client, so an ACL enforced here would silently admit everyone unless it
trusted X-Forwarded-For — and trusting a client-settable header for an ACL is a vulnerability, not
a feature. Use a NetworkPolicy, security group or firewall.
What does work inside Rift:
| Goal | Use |
|---|---|
| Refuse connections from other hosts | --local-only: the admin API, /metrics and --intercept-port bind 127.0.0.1 and ::1 (/metrics since 0.17.0), and they (#1568, #1572), every imposter port and the front door (#1523) refuse non-loopback clients at accept |
| Require a credential on the admin API | --api-key <token> |
| Fail startup if the admin plane is exposed and keyless | --require-admin-auth |
GET /config reports "ipWhitelist": ["*"], which is accurate: every address may connect.
--intercept-port eagerly starts the intercept/TLS-MITM proxy
at boot. It is no longer the only way to enable it: a server started without the flag still exposes
the runtime lifecycle endpoints (POST/GET/DELETE /intercept), so intercept can be turned on at
runtime over the admin API. The flag and the endpoints drive the same single listener.
A third way, and the only declarative one, is an
intercept block in --configfile:
it starts the listener with its rules already installed, so a container needs no post-boot admin
call. The block and these --intercept-* flags are two spellings of one listener — supplying both
is a startup error rather than a silent precedence guess, so pick one. (Each flag also has a
RIFT_INTERCEPT_* environment variable, which counts as supplying it.)
The block can also take its CA from environment variables, by name: "caCertPemEnv": "INTERCEPT_CA_CERT",
"caKeyPemEnv": "INTERCEPT_CA_KEY". This suits ECS/Fargate, which delivers secrets only as environment
variables. Unlike RIFT_INTERCEPT_CA_CERT_PEM, these variables are not flags, so the block may use
them. An unset variable stops the start with an error that names it. POST /admin/reload re-applies
the block's rules but never rebinds the listener or swaps its CA; an edit that changes the listener
too applies neither until a restart.
The intercept proxy is unauthenticated unless you say otherwise¶
The listener has no credential by default — it is off unless asked for, and most uses are a loopback test rig where auth is pure friction. But it is a TLS-MITM proxy: anyone who can reach the port can route traffic through it and be served certificates forged by Rift's CA, which are trusted wherever that CA is installed — and installing it is the whole point of the feature. On a shared or LAN-reachable host, set a credential:
Every CONNECT must then carry Proxy-Authorization: Basic <base64(user:pass)>; anything else gets
407 Proxy Authentication Required. Standard clients do this for you —
HTTPS_PROXY=http://ci:s3cr3t@host:8888, curl -x http://ci:s3cr3t@host:8888, or a JVM
Authenticator. A value with no :, or with a blank half, is a startup error rather than a
silently-disabled gate — as is --intercept-auth without --intercept-port, which would otherwise
read as protection while guarding nothing.
On a shared host prefer RIFT_INTERCEPT_AUTH: a value passed on the command line is visible to
anyone who can run ps.
Note this is Proxy-Authorization, not the admin --api-key. The two are different credentials
on purpose: Proxy-Authorization is hop-by-hop and is consumed here, whereas Authorization is
end-to-end and would be forwarded to every intercepted origin — sending your admin key onward to the
very servers you are intercepting.
--require-admin-auth covers this listener too: a non-loopback intercept bind with no credential
warns by default and refuses to start under that flag.
RC file (--rcfile)¶
--rcfile <FILE> reads defaults from a JSON object, as Mountebank's --rcfile does. Only these
keys are recognised (the snake_case spellings are accepted too):
| Key | Type | Sets |
|---|---|---|
port |
integer 0–65535 | --port |
host |
string | --host |
loglevel / logLevel |
string | --loglevel |
allowInjection |
boolean | --allow-injection |
localOnly |
boolean | --local-only |
requireAdminAuth |
boolean | --require-admin-auth |
apiKey |
string | --api-key |
datadir |
string | --datadir |
configfile |
string | --configfile |
noParse |
boolean | --no-parse |
mock |
boolean | --mock (accepted, no effect) |
debug |
boolean | --debug |
Rules:
- Lowest precedence. A key is applied only when the setting is still at its built-in default, so
a flag or environment variable wins. The check is by value:
--port 2525on the command line does not stop an rcfileportfrom applying, because 2525 is the default. - A missing file warns. A
--rcfilepath that does not exist prints--rcfile <path> not found; starting without itand the server starts without it, as Mountebank does (issue #1574). Only that case: a path that exists but cannot be read (a directory, no permission) still aborts, as below. - Refused, not ignored. A file that exists but cannot be read or parsed, is not a JSON object, or gives a
recognised key the wrong type (
"localOnly": "yes","port": 70000) aborts startup, naming the file — and nothing from it is applied. AnapiKeyof the wrong type is reported by its type, never echoed. - Unknown keys are skipped with a warning on stderr.
- A blank
apiKeyis refused exactly as--api-key ""is (see below). - The file is read before
rift healthcheckcomputes its target, so the probe follows a port the rcfile sets.
API-key authentication¶
--api-key (or Mountebank's spelling --apikey, or MB_APIKEY) requires every admin API request
to carry the token, as the raw value, in the Authorization header or in Mountebank's x-api-key
header. When both headers are sent, Authorization decides alone and x-api-key is ignored: a
request never authenticates on whichever of the two happens to match. Data-plane traffic — direct
imposter ports and the /__rift/:port/... gateway — is not gated by this key. rift
healthcheck, save and replay present the key in Authorization.
A blank value is refused at startup rather than accepted as a key:
--api-key "" (or MB_APIKEY= set-but-empty) would otherwise enable the auth gate and then match
every unauthenticated request, leaving the admin API open while reporting as protected. Whitespace
counts as blank. Omit the flag entirely to run the admin API explicitly unauthenticated; a key that
merely contains spaces is still a valid key and is compared exactly as given.
rift --api-key s3cr3t
curl -H "Authorization: s3cr3t" http://localhost:2525/imposters
curl -H "x-api-key: s3cr3t" http://localhost:2525/imposters
Unauthenticated admin plane on a public interface¶
With no --host the admin API listens on every interface, IPv6 and IPv4 ([::] dual-stack;
0.0.0.0 on a host without IPv6), so a bare rift with no --api-key already serves the
full admin API — which can create imposters and drive the TLS intercept proxy — on every interface
with no authentication. Since 0.17.0 that posture is stated at startup instead of being silent:
WARN the admin API is bound to [::]:2525, which is reachable from outside this host, with no
API key — anyone who can reach that address can create imposters and drive the TLS intercept
proxy. Set `--api-key <token>` (`MB_APIKEY`), or restrict the bind with `--local-only` or
`--host 127.0.0.1`. Set `--require-admin-auth` (`RIFT_REQUIRE_ADMIN_AUTH`) to make this a
startup failure instead of a warning.
The default is a warning, not a refusal: containers require every interface (binding loopback inside Docker makes the published port unreachable), so refusing would break the no-argument invocation and every keyless quickstart. Fleets that want fail-closed opt in:
# Refuse to start unless the admin plane is authenticated or loopback-only
rift --require-admin-auth # errors: every interface with no key
rift --require-admin-auth --api-key s3cr3t # ok — authenticated
rift --require-admin-auth --local-only # ok — not reachable off-host
--require-admin-auth gates on authentication, not on the address: a real --api-key satisfies
it on any bind. Loopback (127.0.0.0/8, ::1) satisfies it with no key. 0.0.0.0 and :: are
unspecified addresses, not loopback, so they are flagged — as is any specific off-host interface
such as 10.0.0.5.
--host may be a DNS name, as in Mountebank: it is resolved once at startup and the admin API binds
the first address it resolves to, so --host localhost binds 127.0.0.1 or ::1, whichever the
resolver returns first. The exposure check above judges that resolved address, so a name that
resolves to a LAN address is warned about, or refused under --require-admin-auth, like the
address itself. A name that does not resolve is a startup error naming it. --local-only still
pins loopback and never looks the name up.
--host also confines imposters, as in Mountebank: an imposter created without a host binds the
server's --host, so --host 127.0.0.1 keeps host-less imposters on loopback as well as the admin
API. An imposter that names its own host ("host": "0.0.0.0") binds that instead. --local-only
does not narrow imposter binds. As in Mountebank, an imposter still listens where its host says,
but refuses every client that is not loopback (127.0.0.0/8, ::1, and their IPv4-mapped forms):
the connection is closed at accept, before TLS, and the request is not recorded. The front door
refuses the same clients, and so do the admin API and /metrics (#1568): their loopback bind alone
would still accept a connection from another local interface's address, routed over loopback. The
intercept proxy does too (#1572): --intercept-port listens on 127.0.0.1 and ::1, and a
listener started at runtime on another host still refuses non-loopback clients.
rift_blocked_connections_total{listener} counts the refusals.
The same rule applies at every door onto the admin plane, so an embedded host gets it too: the
C-ABI rift_serve_admin accepts "requireAdminAuth": true in its options, and an embedder building
an AdminApiServer directly gets it from .with_require_admin_auth(true).
Default TLS for HTTPS imposters¶
An imposter declared with protocol: https terminates TLS. If it carries no cert/key, Rift
falls back to --default-tls-cert / --default-tls-key when set, otherwise to a generated
self-signed certificate. Pass --no-self-signed-tls to refuse such an imposter instead of silently
self-signing: POST /imposters answers 400, and an imposter loaded at startup is skipped with an
error log while the server still starts. See TLS/HTTPS Support.
rift \
--default-tls-cert ./certs/server.pem \
--default-tls-key ./certs/server-key.pem \
--no-self-signed-tls
Examples¶
# Start with custom port
rift --port 3525
# Load configuration and enable injection
rift --configfile imposters.json --allow-injection
# Debug logging
rift --loglevel debug
# Restrict access
rift --local-only
rift --api-key s3cr3t --require-admin-auth
# With persistent data directory (see "Data directory" below)
rift --datadir ./mb-data
# Seed from a config file and persist admin-API imposters
rift --configfile imposters.json --datadir ./mb-data
Data directory (--datadir)¶
--datadir persists imposters created or changed through the admin API: each one is written to
<dir>/<port>.json, and deleting the imposter deletes the file. At startup (and on
POST /admin/reload) every *.json file in the directory is loaded back. Other extensions are
ignored.
A file is never rewritten in place. Each write goes to <port>.json.tmp beside it, is synced to
disk, and is then renamed over <port>.json, so a crash, a full disk or a reload reading at the same
moment sees either the old document or the new one, never a partial one. A write that fails leaves
the old file as it was, and the admin call returns 503. A leftover <port>.json.tmp means a
process died mid-write. It is never loaded, and the next start removes it with a WARN line.
Because each write creates a new file, a mode or ownership set by hand on <port>.json, or a hard
link to it, does not carry over to the next write.
Beside it, <port>.cursors holds the imposter's response cycle positions, so a restart resumes
each stub where it stopped, as Mountebank does. It is runtime state, not config: written every
second while the cycles move and on a graceful stop, through <port>.cursors.tmp the same way, so a
crash can lose up to a second of advances. Each position is keyed by the stub's id, or else by its
content, so a stub edited by hand in <port>.json starts its cycle over and the others resume. A
<port>.cursors that cannot be read is ignored with a WARN line (every cycle starts over); it
never stops the imposter from loading. Deleting the imposter deletes it too. Neither file is ever
loaded as an imposter, and an older Rift ignores both.
For an imposter created with recordRequests: true, <port>.requests.jsonl holds its recorded
requests, so a restart brings them back, as Mountebank does. It is JSON Lines: a {"version":1}
header, then one request per line, as GET /imposters/:port prints it. Rift appends to it about
once a second, holding back a request still waiting for its response for up to two seconds, and
writes the rest on a graceful stop; a crash can lose the last ~2 seconds of requests (Mountebank
writes each one before answering it). A request that takes longer than that to answer (a long wait
behavior, a slow proxy) is written without its status and latencyMs, and that line is not
updated later. What a restart restores is held to --journal-max-bytes
like live recording, oldest first, and the file is compacted once it is more than twice the size of
what the journal retains (and over 2 MiB), so the disk holds at most about twice the budget per
imposter. DELETE /imposters/:port/savedRequests removes the file, a scoped clear rewrites it, and
deleting the imposter deletes it. numberOfRequests still starts at 0 after a restart, as in
Mountebank. A truncated last line (a crash mid-write) is dropped; any other unreadable line is
skipped with a WARN; the file never stops the imposter from loading. Rewrites go through
<port>.requests.jsonl.tmp, which the next start removes. A journal injected by an embedder
(with_request_journal) and --configfile imposters are not written.
The directory is keyed by port, so each file is held to that:
- It holds one imposter object (not an
{"imposters": [...]}wrapper), as plain JSON. EJS tags are not rendered in datadir files. - It must declare its port. A file with no
port, or"port": 0, is refused — it would be auto-assigned a port and written again as a second file on every load. - It must be named after that port.
4545.jsonmust declare"port": 4545; a file named anything else is refused with the name it should have.
A datadir file was written by a Rift engine, possibly an older one, so it is read the way a store
replays what it holds. It is held to the admission checks (a predicate or behavior selector that
does not compile, a stub with no responses), but not to the door-only rules, which judge a shape as
written and were added for shapes older releases admitted and wrote. A pre-0.18.0
{"repeat": n} behaviors element, a repeat: null, or a behaviors element holding only an unknown
key all still load from a datadir, though POST /imposters and --configfile refuse them, as
Mountebank does. Mountebank does not validate its datadir on load at all.
At startup a file that breaks these rules — or that cannot be parsed, needs --allow-injection
without it, or fails to create — is skipped: the server still starts, and one ERROR line lists
every skipped file with its reason. POST /admin/reload is stricter: one bad file refuses the whole
reload, naming the file, and the running imposters are left as they are. A missing directory is
created, empty.
With both --configfile (or --imposters) and --datadir, config-file imposters are not
written to the data directory, and POST /admin/reload re-reads both stores. When both declare the
same port, the config-file imposter is served and the datadir file is skipped and named.
At startup, every imposter that declares a port — from either store — is created before any
imposter that does not, so an auto-assigned port can never take a port another imposter asked for.
"port": 0 means "auto-assign", the same as omitting the port.
Body size limits¶
Rift buffers every request body, admin body and proxied upstream response in memory, because predicates, scripts and the request journal all need the whole body. Two limits bound that (issue #1439):
--max-body-size(default 512 MiB) refuses one body over it with413 Payload Too Large. Mountebank has no limit of its own, but it crashes past about 512 MiB (V8's string limit), so the default refuses nothing Mountebank can serve. A request that declares its length is refused before the body is read: a client sendingExpect: 100-continueis never told to send it.--body-memory-budget(default 1 GiB) is shared by every body being buffered at once, across all imposters, the proxy and the admin API. When it is used up, a request gets503 Service UnavailablewithRetry-After: 1instead of the process running out of memory. A request that declares its length waits up to 2 s for room first; a chunked one is refused at once. It must be at least--max-body-size, or no body under the ceiling could be admitted, and Rift refuses that configuration at startup, so raising--max-body-sizepast 1 GiB also needs a larger budget.0makes it unbounded.
A request body sent with Content-Encoding: gzip or br is decoded before matching, and it is the
decoded size that --max-body-size limits and the budget is charged for (issue #1584).
A proxied upstream response is held to the same limits, but never waits for the budget, and a
refusal there fails the proxy with 502 like any other upstream failure. The 413 and 503
responses close the connection, and over HTTP/1.1 Rift first reads and discards the rest of the
body for up to 2 s (1 s of silence, 64 MiB), so a client still writing it receives the status
rather than a connection reset. A refused connection that is still draining holds an imposter
delete, or a shutdown, for up to those 2 s. Embedders set the same limits with
ImposterManager::with_body_limits, or maxBodySize / bodyMemoryBudget in the C-ABI
ServeOptions. The intercept listener keeps its own 1 MiB cap.
A lookup behavior's CSV file is read under the same limits (#1654): a file larger than
--max-body-size is not read (a warning is logged and the tokens are left in place), and while the
budget is used up the request gets 503 with Retry-After: 1. The cache of parsed files holds at
most --max-body-size. See Behaviors.
Environment Variables¶
A flag given on the command line wins over its environment variable, and the environment variable
wins over the built-in default (and over an --rcfile value — see RC file).
| Variable | Description | Default |
|---|---|---|
MB_PORT |
Admin API port | 2525 |
MB_HOST |
Admin API and host-less imposter bind IP address (IPv4, or IPv6 ::1 / [::1]) |
every interface, IPv6 and IPv4 ([::] dual-stack; 0.0.0.0 on a host without IPv6) |
MB_CONFIGFILE |
Imposter config file | |
RIFT_IMPOSTERS |
Imposter source URIs (env alias of --imposters) |
|
MB_DATADIR |
Persistent storage directory | |
MB_ALLOW_INJECTION |
Enable injection (true/false) |
false |
MB_LOCAL_ONLY |
Localhost only: admin API and /metrics on loopback; imposters and the front door refuse non-loopback clients |
false |
RIFT_REQUIRE_ADMIN_AUTH |
Refuse to start on a keyless non-loopback admin bind (env alias of --require-admin-auth) |
false |
MB_LOGLEVEL |
Log level (env alias of --loglevel). Set-but-empty means info |
info |
MB_APIKEY |
Admin API authorization token (see --api-key) |
|
RIFT_SCRIPTS_DIR |
Root directory for admin-API file:/ref: script resolution and for require() and lookup CSV files in admin-API, --datadir and https: imposters (env alias of --scripts-dir); paths escaping it are rejected |
|
RIFT_DEBUG |
Enable debug mode (truthy: 1/true/yes/on); same as --debug. Adds an x-rift-script-trace response header and makes response-template errors return a request-time error instead of an empty substitution. Does not add Mountebank's per-stub matches |
off |
RIFT_RUNTIME |
Runtime topology (env alias of --runtime): work-stealing or per-core[=N] (RFC-712; experimental) |
work-stealing |
RIFT_RUNTIME_AFFINITY |
Pin per-core worker threads to CPU cores (env alias of --runtime-affinity) |
off |
RIFT_METRICS_PORT |
Prometheus metrics port | 9090 |
RIFT_FRONT_DOOR |
Front-door bind address (env alias of --front-door): HOST:PORT or a bare port |
off |
RIFT_DEFAULT_TLS_CERT |
Default TLS certificate (PEM) for HTTPS imposters | |
RIFT_DEFAULT_TLS_KEY |
Default TLS private key (PEM) | |
RIFT_NO_SELF_SIGNED_TLS |
Disable self-signed TLS fallback (true/false) |
false |
RIFT_UPSTREAM_CA_FILE |
Extra CA certificate(s) (PEM file) trusted for outbound TLS — proxy stubs and --configfile URLs. Appended to the OS trust store |
|
RIFT_UPSTREAM_TLS_SKIP_VERIFY |
Accept any certificate on outbound TLS (true/false); development only |
false |
RIFT_MAX_BODY_SIZE |
Largest request, admin or proxied-response body accepted, in bytes (env alias of --max-body-size); over it is a 413 |
536870912 |
RIFT_JOURNAL_MAX_BYTES |
Bytes of recorded requests each imposter keeps, oldest evicted first (env alias of --journal-max-bytes); a body larger than the whole budget is recorded without it and flagged _bodyOmitted |
805306368 |
RIFT_BODY_MEMORY_BUDGET |
Memory, in bytes, shared by every body buffered at once (env alias of --body-memory-budget); used up, a request gets 503 + Retry-After. At least RIFT_MAX_BODY_SIZE; 0 = unbounded |
1073741824 |
RIFT_INTERCEPT_PORT |
Start the intercept/TLS-MITM proxy on this port (epic #394) | |
RIFT_INTERCEPT_AUTH |
user:pass required in Proxy-Authorization on every CONNECT to the intercept proxy (env alias of --intercept-auth); open when unset |
|
RIFT_INTERCEPT_CA_CERT |
PEM CA certificate file for interception (with RIFT_INTERCEPT_CA_KEY) |
|
RIFT_INTERCEPT_CA_KEY |
PEM CA private key file for interception | |
RIFT_INTERCEPT_CA_CERT_PEM |
Inline PEM CA certificate (the bytes, not a path; with RIFT_INTERCEPT_CA_KEY_PEM) — mutually exclusive with the _CA_CERT/_CA_KEY file pair |
|
RIFT_INTERCEPT_CA_KEY_PEM |
Inline PEM CA private key for interception | |
RIFT_ADMIN_URL |
Admin API URL that rift-tui connects to (env alias of its --admin-url; see TUI) |
http://localhost:2525 |
RIFT_TRUSTSTORE_PASSWORD |
Password for rift intercept-ca export (same as --password, kept out of argv) |
changeit |
RIFT_DISABLE_HTTP2 |
Force HTTP/1-only listeners, disabling HTTP/2 & h2c auto-negotiation (truthy: 1/true/yes/on). On HTTPS listeners it also removes h2 from the ALPN offer, so a client offering both protocols negotiates http/1.1 instead of being handed an h2 the server will not speak. A client offering only h2 is refused at the handshake with no_application_protocol — a loud failure rather than a protocol mismatch |
off |
RIFT_TCP_BACKLOG |
Listen backlog for the accept loop (positive integer) | 1024 |
RIFT_TCP_NODELAY |
TCP_NODELAY on accepted sockets; true/1/on enables, false/0/off disables (case-insensitive) |
on |
RIFT_HTTP_MAX_BUF |
Per-connection HTTP read/write buffer cap, in bytes (positive integer; floored at hyper's 8 KB minimum). Bounds per-connection memory at high connection counts | 65536 |
RIFT_HTTP_HEADER_TIMEOUT |
Seconds to wait for a client to finish sending request headers before closing the connection (slowloris hygiene; positive integer). Also bounds the HTTP/1-vs-HTTP/2 detection window at the start of a connection, so a client that completes the handshake and then sends nothing is closed rather than parked | 30 |
RIFT_MAX_CONNECTIONS |
Cap on concurrently-served connections per listener (positive integer). Unset means unlimited; at the cap the server stops accepting until a connection closes, so overload waits in the kernel backlog rather than piling up. Applies to the intercept listener too, as of #1030 | unlimited |
RIFT_STRICT_BEHAVIORS |
Force strict mode process-wide (truthy: 1/true/yes/on): a decorate/shellTransform/binary-base64-decode failure returns 500 instead of the lenient fallback body |
off |
NO_COLOR |
Suppress ANSI color and the decorative banner in rift-verify / rift-lint output |
|
RUST_LOG |
A full tracing filter (e.g. warn,rift::script=debug). When set it replaces --loglevel/--debug; a value that does not parse is refused at startup |
unset |
RIFT_DISABLE_HTTP2 is an escape hatch for clients or intermediaries that mishandle HTTP/2; see
HTTP/2 and h2c. RIFT_TCP_BACKLOG and
RIFT_TCP_NODELAY are socket-tuning knobs covered under
Performance → Runtime socket tuning.
The five tuning knobs above (RIFT_TCP_BACKLOG, RIFT_TCP_NODELAY, RIFT_HTTP_MAX_BUF,
RIFT_HTTP_HEADER_TIMEOUT, RIFT_MAX_CONNECTIONS) fall back to their defaults when unset.
A value that is set but cannot be used — not a number, out of range, or an unrecognised boolean
spelling — also falls back, but logs a WARN naming the variable, the value and the reason, so a
typo is visible in the log rather than only in behaviour that does not match the configuration.
RIFT_MAX_CONNECTIONS=0 is the one exception: it reads as "no cap", which is what it does, so it
is accepted silently.
RIFT_HTTP_HEADER_TIMEOUT is applied in three places on a new connection: to the
HTTP/1-vs-HTTP/2 detection window; then, if the connection resolves to HTTP/1, by HTTP/1's own
header timer, which can only start once detection has resolved; and, if it resolves to HTTP/2, as
both the keep-alive ping interval and the pong timeout. So the worst case for a client that goes
silent at exactly the wrong moment is up to two header timeouts before the connection is
closed, not one — on either protocol. The phases are not netted against each other deliberately:
subtracting elapsed time would make a very small RIFT_HTTP_HEADER_TIMEOUT behave erratically, and
bounding each phase separately is easier to reason about than a shared budget.
The HTTP/2 leg needs its own bound because HTTP/2 has no equivalent of HTTP/1's header timer: once
the preface is read, a peer that never opens a stream is not waiting on a request head that a timer
could bound. The keep-alive ping is the only mechanism that reaches it. A consequence worth knowing
about: idle HTTP/2 connections are now pinged every RIFT_HTTP_HEADER_TIMEOUT (30s by default).
A live client answers and is unaffected; the traffic is two frames per interval per idle
connection.
RIFT_STRICT_BEHAVIORS and the per-imposter strictBehaviors field combine with OR — either
being set enables strict mode. See
Rift Extensions → Strict Behaviors
for the full semantics.
Docker Example¶
docker run \
-e MB_PORT=2525 \
-e MB_ALLOW_INJECTION=true \
-e RUST_LOG=debug \
-p 2525:2525 \
-p 9090:9090 \
zainalpour/rift-proxy:latest
Docker Compose Example¶
version: '3.8'
services:
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525"
- "4545:4545"
- "9090:9090"
environment:
- MB_PORT=2525
- MB_ALLOW_INJECTION=true
- RUST_LOG=info
volumes:
- ./imposters.json:/imposters.json
command: ["--configfile", "/imposters.json"]
Logging Configuration¶
Log Levels¶
| Level | Description |
|---|---|
error |
Only errors |
warn |
Warnings and errors (warning is also accepted) |
info |
Standard operation (default) |
debug |
Detailed debugging |
trace |
Very verbose (development) |
Levels are case-insensitive. Any other value is refused at startup with an error listing the
accepted ones — it used to fall back to info silently. An empty value (for example
MB_LOGLEVEL=${LOG_LEVEL} with LOG_LEVEL unset) means info.
The filter actually used is chosen in this order:
RUST_LOG, when it is set — a fulltracingfilter. A value that does not parse, or is not valid UTF-8, is refused at startup rather than ignored. (RUST_LOG=set to the empty string is accepted.)--debug, which meansdebug.--loglevel/MB_LOGLEVEL/ the rcfileloglevelkey.info.
--loglevel is validated even when RUST_LOG overrides it, so a typo fails the same way in every
environment.
Module-Specific Logging¶
RUST_LOG targets are matched by prefix. Rift's own log lines use the crate module paths
(rift_http_proxy::…, rift_mock_core::…) plus the explicit targets rift::script,
rift::template and rift::state_ops:
# Debug everything Rift logs (the prefix `rift` covers all of the above)
RUST_LOG=rift=debug rift
# Debug script execution only
RUST_LOG=info,rift::script=debug rift
# Debug imposter handling in the engine, admin API at info
RUST_LOG=info,rift_mock_core::imposter=debug rift
Log lines are plain text on stdout (the tracing default format); --log <FILE> writes the same
lines to a file as well (without ANSI colour codes). GET /logs serves the most recent lines from
memory unless --nologfile is given; see GET /logs.
Health Check¶
Rift provides health endpoints:
# Admin API health — answers {"status":"ok"}
curl http://localhost:2525/health
# Metrics endpoint (separate listener)
curl http://localhost:9090/metrics
With --api-key set, every admin API path — /health and / included — requires the
Authorization header and answers 401 without it. The metrics listener on --metrics-port is
not gated by the key.
Signal Handling¶
SIGTERM and SIGINT (Ctrl+C) shut the server down gracefully (issue #1155):
- it stops accepting on the admin API, the metrics listener and the front door, and gives connections already in flight on them a short, bounded grace — about three seconds at worst;
- imposter connections are closed rather than drained — a mock server has no in-flight work worth holding a shutdown for, which is also what Mountebank does;
- persisted imposters in
--datadirare left exactly as they are, so the next start serves them; - the
--pidfileit wrote is removed, and the--logfile is flushed; - the process exits
0.
This holds for a rift that is a container's PID 1, too, so docker stop and a Kubernetes pod
termination are prompt, and no init process (docker run --init, init: true) is needed.
Exit Codes¶
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Any runtime failure: a refused flag value, config file or rcfile, a port that cannot be bound, a failed stop/save, an unhealthy healthcheck |
2 |
The command line itself could not be parsed (unknown flag, missing value, conflicting flags) |
The error is printed to stderr in every case; the exit code does not distinguish the cause further.
A server stopped by SIGTERM or SIGINT exits 0: that is a clean shutdown, not a failure.
Subcommands¶
Rift supports several subcommands for server management.
Server flags such as --port, --host, --configfile, --rcfile and --api-key may go before
or after the subcommand, matching Mountebank's mb start --port 2525 --configfile imposters.json:
rift --port 3525 save and rift save --port 3525 are the same command. A subcommand's --help
lists them under "Server options". The environment variables work as usual. Each subcommand's own
flags are listed below.
start¶
Start the Rift server (default behavior when no subcommand is specified):
rift start
rift --port 3525 --configfile imposters.json start
# The Mountebank form: server flags after the subcommand
rift start --port 3525 --configfile imposters.json
stop¶
Stop a running Rift server using its PID file. A server only writes a PID file when it was started
with --pidfile, so start it that way if you intend to use stop/restart:
rift --pidfile /var/run/rift.pid &
# Stop using that PID file
rift stop --pidfile /var/run/rift.pid
# Without --pidfile, stop reads ./rift.pid
rift stop
The PID file is written only once the server is ready: the admin API and every startup imposter
(--configfile, --imposters, --datadir) are bound and serving. Tooling may wait for the file as
a readiness signal, as Mountebank's build plugins do with mb. A startup that fails (a bad config
file, a port in use, a refused injection) writes no PID file.
stop sends SIGTERM (taskkill /F on Windows) and waits for the process to exit before it
returns — up to 5 seconds, above the server's own shutdown bound — then removes the PID file (the
server usually removes it first, which is fine). A process still running after that is reported as
an error and its PID file is kept, rather than being assumed gone. A PID file whose process is
already gone is treated as stale: it is removed and stop succeeds. A missing PID file means
nothing is running: stop logs nothing to stop and succeeds, as mb stop does. A non-positive
PID, or a process rift is not permitted to signal, is an error, and the file is left in place.
restart¶
Stop the server named by the PID file, then start a new one in this process with the other flags given:
rift restart --pidfile /var/run/rift.pid
# Other server flags may go on either side of the subcommand
rift restart --configfile imposters.json --pidfile /var/run/rift.pid
A missing PID file is not an error for restart — there is nothing to stop, so it just starts.
save¶
Save the running server's imposters to a file for later replay. The server is found through
--host/--port (so MB_HOST/MB_PORT and --rcfile apply):
# Save imposters to ./mb.json (the default, as in Mountebank)
rift save
# Save to a named file
rift save --savefile recorded.json
# Save from a server on another port
rift save --port 3525
# Drop `proxy` responses, keeping what they recorded (stubs left with no response are dropped)
rift save --savefile mocks.json --remove-proxies
| Flag | Description | Default |
|---|---|---|
--savefile <FILE> |
Output file | mb.json |
-r, --remove-proxies |
Request the removeProxies=true view (alias: --removeProxies) |
off |
A keyed server needs no extra flag: save presents the admin API key the process already holds
(MB_APIKEY, --api-key, or an rcfile apiKey), as healthcheck does (issue #1154). A non-2xx
answer (for example 401 when no key or the wrong one is set) fails the command instead of writing
the error body to the file.
replay¶
Start a server with the saved imposters loaded:
replay takes the global --configfile (before or after the subcommand; MB_CONFIGFILE and an
--rcfile configfile also count). Without one it exits with status 1 and Error: rift replay needs --configfile <file>.
This is rift --configfile recorded.json under another name: it starts a new server rather than
switching a running Mountebank-style server from recording to replaying.
script¶
Validate and run _rift.script scripts outside a running server (no admin API, no imposter) — the
authoring loop from Scripting. Two actions:
rift script check <target> — statically validate a raw script file (.rhai/.js) or a
config file (JSON/YAML) with _rift.script entries: engine syntax, entrypoint presence/arity for
the intended hook, and (for a config) state-used-without-flowState. Exits non-zero on any
error — so a script whose entrypoint is misnamed fails here instead of at request time.
rift script check scripts/fail-twice.rhai
rift script check scripts/rate-limit.js --hook respond
rift script check imposters.yaml # every _rift.script in the config
| Flag | Description | Default |
|---|---|---|
--hook <HOOK> |
Entrypoint to check a raw script against. Only respond is dispatched at request time, so any other value is rejected — for a config target too, where the flag is redundant because every _rift.script entry is respond-position |
respond |
rift script check also honours the global --no-parse (alias --noParse, before or after the
subcommand): it loads a config target verbatim, skipping EJS preprocessing, as rift --no-parse
does — for a config that contains a literal <%. A raw script is never preprocessed, so the flag is
refused there.
rift script run <target> — execute a script against a fixture request and seeded flow state,
printing the decision, the mutated flow state, captured ctx.logger output, and the execution
duration. No server runs.
rift script run scripts/fail-twice.rhai --state attempts=2
rift script run scripts/echo.js --request fixtures/get-resource.json --flow-id t1
| Flag | Description | Default |
|---|---|---|
--request <FILE> |
JSON file with the request-object shape scripts see ({method, path, headers, query, pathParams, body}; all fields optional). headers holds one entry per name — a name spelled twice (X-Trace and x-trace) is rejected |
empty GET / |
--state <KEY=VALUE> |
Seed flow state before running (repeatable); the value is parsed as JSON when it parses, else stored as a string | |
--flow-id <ID> |
Flow id the seeded state and the script's ctx.state/ctx.store calls use |
cli |
--engine <ENGINE> |
Script engine (rhai/js); inferred from the file extension when omitted |
(from extension) |
--hook <HOOK> |
Entrypoint to run. Only respond is wired across both engines, so any other value is rejected |
respond |
healthcheck¶
Probe a running server's admin API and exit 0 when it answers 2xx, 1 otherwise. This is what
the container images run as their HEALTHCHECK — the probe is built into the binary so the image
needs no shell and no curl, which is what lets the -static image be FROM scratch
(see Docker).
With no arguments it probes /health on the admin API, reading --host/--port (and therefore
MB_HOST/MB_PORT, and an --rcfile that sets them) exactly as the server does — so inside a
container rift healthcheck needs no configuration. With no --host, or a bind-any host (0.0.0.0,
::), it probes 127.0.0.1, since that is where a server bound to every interface answers.
rift healthcheck # probes http://127.0.0.1:2525/health
MB_PORT=3000 rift healthcheck # follows MB_PORT
rift --rcfile /etc/rift/rc.json healthcheck # follows the port that file sets
rift healthcheck --url http://localhost:9090/metrics # probe something else
Pass the same --rcfile the server was started with, or the probe knocks on the default port
(issue #1133). An rcfile the server would refuse (a wrong-typed key, an unparseable file) refuses the
probe too, and reports unhealthy: a server started with that file would not have started either.
A keyed server needs no extra configuration (issue #1154). The probe presents the admin API
key it already has — MB_APIKEY, --api-key, or an rcfile apiKey, all read exactly as the server
reads them — so a container that sets MB_APIKEY keeps its built-in HEALTHCHECK healthy with no
change to the image or the command. There is deliberately no key flag on the subcommand: it would
duplicate the top-level one and invite putting a secret in argv, where docker inspect shows it.
The key goes only to the URL derived from --host/--port. An explicit --url is an arbitrary
target — pointing it at the unauthenticated metrics listener is the common use — and attaching the
admin secret to it would leak the key, so an explicit --url is never sent one. If you point --url
at the admin API of a keyed server it gets 401 and reports unhealthy; drop --url instead. A 401
with no key configured says which setting to add.
| Flag | Description | Default |
|---|---|---|
--url <URL> |
URL to probe instead of the admin API's /health |
(from --host/--port) |
--timeout <SECONDS> |
Give up and report unhealthy after this long. Kept under the images' HEALTHCHECK --timeout=3s so a hung server makes the probe report the verdict itself instead of being killed mid-probe |
2 |
intercept-ca¶
Make a persistent intercept CA and the truststores a SUT needs, offline — no server starts and nothing touches the admin API. This is for a SUT that has to trust the CA before rift runs (a container whose JVM reads its truststore once at startup), where the CA a listener generates for itself arrives too late.
rift intercept-ca generate --out-dir ./ca --cn "Acme CI Intercept CA" --validity-days 365
rift intercept-ca export --cert ./ca/ca-cert.pem --format jks --out ./truststore.jks \
--merge-system-cas /etc/ssl/certs/ca-certificates.crt
rift intercept-ca export --cert ./ca/ca-cert.pem --format pkcs12 --out ./truststore.p12 --password s3cret
generate writes ca-cert.pem and ca-key.pem (the key with mode 0600 on Unix) and prints how to
start rift with them. The key is never printed. The CA is an ECDSA P-256 CA valid from five minutes
ago (for clock skew) until --validity-days from now.
export reads the certificate only — it never needs the key — and writes one trusted entry for the
CA plus, with --merge-system-cas, one per certificate in each PEM bundle, named by its subject CN
(lower-cased, de-duplicated). Without a bundle the store trusts the intercept CA alone, so a JVM that
uses it as its default truststore can no longer reach anything real. The JDK's cacerts is a
keystore, not a PEM bundle; pass a PEM bundle such as Debian's /etc/ssl/certs/ca-certificates.crt.
Neither verb overwrites an existing file unless --force is given; generate stages both files and
moves them into place together, so a failed run never leaves a new key beside an old certificate.
--merge-system-cas accepts plain CERTIFICATE blocks only and refuses a bundle holding anything
else (OpenSSL's TRUSTED CERTIFICATE format, keys) or a block that is not a valid certificate,
rather than writing a store that trusts less than asked or that the SUT cannot load. A certificate
that appears twice (or is the CA itself) is stored once.
Exit codes: 0 done; 2 refused (an output exists, an input cannot be read or holds no valid
certificate, an invalid option); 1 generating or writing the output failed.
| Verb | Flag | Description | Default |
|---|---|---|---|
generate |
--out-dir <DIR> |
Directory for ca-cert.pem and ca-key.pem (created if missing) |
(required) |
generate |
--cn <NAME> |
The CA's subject common name | Rift Intercept CA |
generate |
--validity-days <DAYS> |
How long the CA stays valid, from now (at least 1) | 3650 |
| both | --force |
Overwrite existing output files | off |
export |
--cert <FILE> |
The CA certificate PEM | (required) |
export |
--format <jks\|pkcs12> |
Truststore format | (required) |
export |
--out <FILE> |
Where to write the truststore | (required) |
export |
--password <PASSWORD> |
Truststore password; prefer RIFT_TRUSTSTORE_PASSWORD, since argv shows in ps |
changeit |
export |
--merge-system-cas <BUNDLE> |
Also trust every certificate in this PEM bundle; repeatable | none |
export |
--alias <ALIAS> |
The CA's alias in the store (lower-case: the JVM lowercases JKS aliases) | rift-intercept-ca |
Additional CLI Tools¶
Rift includes additional CLI tools for working with imposters:
rift-verify¶
Test imposters by making requests and verifying responses.
rift-verify [OPTIONS]
Options:
-a, --admin-url <URL> Rift admin API URL [default: http://localhost:2525]
-p, --port <PORT> Verify specific imposter port only
-c, --show-curl Show curl commands for each test
-v, --verbose Verbose output with pass/fail details
-t, --timeout <SECS> Request timeout in seconds [default: 10]
-o, --output <FMT> Output format: text (default), json
--dry-run Show what would be tested without making requests
--skip-dynamic Skip stubs with dynamic responses instead of requesting them (off by default)
--verify-dynamic Opt-in: assert dynamic stubs with a mock upstream, _verify or fault checks
--status-only Only verify status codes (ignore body/headers)
--gateway Send requests through the admin port's /__rift/<port>/ gateway
--insecure Accept self-signed/invalid TLS certificates (https imposters)
--space <SPACE> Correlation value sent to imposters whose flowIdSource is a header [default: rift-verify]
--flow-id-header <FLOW_ID_HEADER> Fallback correlation header name when it cannot be discovered
--demo Run demo showing enhanced error output
-h, --help Print help
-V, --version Print version
Examples:
# Verify all imposters
rift-verify
# Verify specific imposter with curl commands
rift-verify --port 4545 --show-curl
# Dry run to see test plan
rift-verify --dry-run --verbose
# Skip dynamic stubs (proxy, inject, script)
rift-verify --skip-dynamic
# Assert dynamic stubs instead of skipping them
rift-verify --verify-dynamic
# Status-only mode for cycling responses
rift-verify --status-only
# Machine-readable summary for CI (JSON on stdout, progress on stderr)
rift-verify -o json
With -o json, rift-verify writes a single summary object to stdout —
{ "imposters", "stubs", "tests", "passed", "failed", "skipped" } — and routes all progress and
banner output to stderr, so it pipes cleanly into other tools. Color and the decorative banner are
also suppressed automatically when stdout is not a TTY (piped) or when NO_COLOR is set.
A stub whose response is dynamic (proxy/inject/script/cycling/faults, and the
repeat/decorate/copy/lookup/shellTransform behaviors unless null or an empty list) has
output that isn't a static function of the stub. By default rift-verify still sends it a request,
and asserts only what can be known: any 2xx status passes (a _rift.fault.tcp reset passes as the
expected outcome), plus the headers and body of its first is response when it has one. So a script
or inject stub that deliberately answers a non-2xx status is reported as a FAIL.
--skip-dynamic reports every such stub as a SKIP instead; it is a plain switch and takes no value.
--verify-dynamic asserts those stubs properly, using
three mechanisms — an embedded mock upstream for proxy stubs (verifying the proxied response and,
when predicateGenerators is set, the recorded-stub prepend); a _verify expectation sequence
(see below) run against a freshly recreated imposter for inject/script/decorate/cycling/stateful
stubs; and deterministic (probability: 1.0 or unset) _rift.fault assertions for latency/error/tcp
faults. Each check runs against a throwaway imposter that is torn down afterward, so it never mutates
the imposters under test. A dynamic stub with none of these assertable markers is still surfaced as a
visible SKIP in the output rather than silently ignored.
See Stub Analysis for details, including the _verify
annotation schema.
rift-lint¶
Validate imposter configuration files before loading.
rift-lint <path> [OPTIONS]
Arguments:
<path> Path to imposter file or directory (required)
Options:
-f, --fix Fix issues automatically where possible
-o, --output <FMT> Output format: text (default), json
-e, --errors-only Only show errors (hide warnings)
-s, --strict Strict mode - treat warnings as errors
--no-parse Lint files verbatim, without rendering EJS tags, as `rift --no-parse` loads them (alias: --noParse)
-h, --help Print help
-V, --version Print version
Examples:
# Lint all imposters in directory
rift-lint ./imposters/
# Strict mode for CI/CD (exits 1 on warnings)
rift-lint ./imposters/ --strict
# JSON output for tooling integration
rift-lint ./imposters/ --output json
# Auto-fix header type issues
rift-lint ./imposters/ --fix
# Only show errors, hide warnings
rift-lint ./imposters/ --errors-only
See Configuration Linting for details.