probe

Ask the source a question whose answer depends on its data, and keep the answer so that verify can ask a container the same question and compare. A container that starts on an empty data directory is running, built from the right image and passes every static check. A question such as "how many keys does Redis hold" or "what does GET / return" is what tells it from a correct one. vrtmv cannot work that question out for your application, so you supply it as a command.

It runs locally, calls no API and does not bill.

# 1. on the source, while it is running
vrtmv probe record --probes probes.json --ssh root@source -o probe-record.json

# 2. later, against the container you started from the rebuilt image
vrtmv verify --attestation out/vrtmv-attestation.json --root /mnt/rebuilt \
  --probe-record probe-record.json --probes probes.json --probe-container web

The probes file

{"probes": [
  {"id": "keys", "description": "how many keys Redis holds", "command": ["redis-cli", "DBSIZE"]},
  {"id": "home", "command": ["sh", "-c", "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/"], "timeout_secs": 10}
]}

id is 1 to 64 letters, digits, -, _ or ., and names the probe in every report. command is an argument list, run as given: there is no shell unless you name one. timeout_secs defaults to 30 and is from 1 to 3600. description is for you and is not kept. A file with no probe, a repeated id, an empty command or a timeout outside 1 to 3600 (0 included) is refused before anything runs.

The comparison verify makes is byte for byte on what the command prints to standard output. vrtmv does no trimming, no pattern matching and no tolerance. So make each command print only what should be the same on the source and in the container: a count, a status code, a checksum of a page. A probe that prints a timestamp, a process id, an uptime or a hostname will differ every time.

A definition that names a credential is not run: passing a credential to the source or to a container is not built. If a probe needs to log in, arrange that on the source and in the container (a client configuration file, for example) and keep it out of command.

record

FlagPurpose
--probes <path>The probes file.
--ssh <user@host>The running source host. --ssh-port, --ssh-key and --jump as for other commands.
--container <name>A source that is itself a running container. Probes run through podman exec.
--runtime <podman|docker>The runtime for --container. Default podman.
--root <dir>, --image <file>A source that is not running. Nothing is run; see below.
-o, --out <path>Where to write the probe record.
--jsonPrint the record as JSON instead of a text summary.

Each probe is run once on the source. An answer is recorded only when the command exits 0 and prints something. Every other outcome is written as not_recorded with the reason:

  • the command exited non-zero, or printed nothing;
  • it could not be run: the binary is not there (exit 127), cannot be executed (126), ssh could not connect (255), or the runtime could not exec in the container (125);
  • it did not finish within its timeout, was killed by a signal, or printed more than 1 MiB. Over --ssh the timeout covers connecting and logging in as well as the command, and the reason says so: a host that does not answer, or an SSH agent that does not answer, uses all of it before the command starts. ssh asks an agent named by SSH_AUTH_SOCK first even when --ssh-key is given;
  • the source is a mounted root or an image. Those are not running, so nothing there can answer, and the command is not run anywhere.

A probe that fails on the source is not recorded on purpose. If its failure were kept as the answer, the same failure in the container would be a match.

record exits 0 when every probe was recorded and 1 otherwise. The record is written either way.

What the command prints to standard error goes to your terminal and nowhere else.

What the record holds

For each probe: its id, a sha256 of its definition, and either the answer or the reason there is none. An answer is the exit status, the number of bytes printed, their sha256, and the time. A reason is a code, with the exit status or the timeout where there is one ({"code": "exited", "exit": 4}); the words you see for it are written by the vrtmv that prints them. The record does not hold the command, its arguments, or what it printed. That is deliberate: a command line can carry a password and the output can be customer data, and the record is a file you hand to someone else.

Two things in the record can be guessed at by someone who holds it:

  • The sha256 and the byte count of the output do not hide a short answer. A key count or a status code can be found by trying the likely values.
  • The sha256 of the definition is over the command, word for word, and timeout_secs. It does not hide a command either: someone who can guess the whole command line can confirm the guess, and if the only part they do not know is a short password written into it, they can try passwords. Keep credentials out of command.

verify reads the commands from the probes file again, and runs a probe in the container only when the file defines it with the same command and the same timeout_secs as when the record was taken. A probe whose definition changed is not_compared, with a reason that names it. id and description are not part of the definition's sha256.

verify refuses a record it cannot hold to this form: an id that is not a probe id, a reason that is not one of the codes, a time or a sha256 that is not shaped like one, a record from --root or --image with an answer in it, or a record with no definition sha256 (one written before this was added; take it again).