Skip to content

What a support bundle contains

morzer support bundle writes one archive you can hand to a stranger. This page is that archive's contract. It is generated from the inventory in the code, so nothing can be collected without a row here.

The list may grow. It may only shrink with a version bump: an operator who has read it once will not read it again, and a component quietly removed is a promise quietly broken in the direction of "we sent less than you thought" — which is the harmless direction only until somebody is debugging from an archive that no longer has what they need.

Never included

Read this first. Inclusion is an allowlist, so these are excluded by not being listed — they are named anyway, because "is the age identity in there?" deserves an answer somebody can check rather than a promise. Adding a collector for one of these fails the build.

What Component Why
— The age identity The key every secret in this installation is encrypted to. Sending it converts an archive that is safe to hand to a stranger into the machine's crown jewels in a ticket system.
— Secret ciphertext Useless to a vendor -- they cannot decrypt it -- and everything to an attacker who later gets the identity. Nothing is gained by including it, which is the easiest kind of refusal to keep.
— The machine's signing key 0028's per-installation identity. A copied signing key signs from anywhere, so an archive carrying it would let its own recipient forge statements attributed to this machine.
— Backup target credentials 0009 put them in the recovery export, so the export is adjacent to this archive and must not be mistaken for includable. They are write access to the place the backups live.
— The recovery export and its recipients' private material 0017's artifact exists to rebuild a machine. An archive that carried it would be a rebuild kit for this installation, addressed to whoever is reading the ticket.
— The render directory Where secrets are rendered as plaintext for the runtime to read, on tmpfs, for as long as the deployment is up. It is the one place on the machine where every secret exists in the clear.

Included

Every one of these is produced by the manager and already structured. No part of it is raw output from your product.

File Component Why
manifest.yaml The resolved manifest What the release actually declares after templating, which is the document every other answer is relative to. A vendor reading a bundle without it is guessing which of their own releases this is.
installation.yaml Installation state What the operator chose: product, layout, policy, targets by name. It holds no secret value and cannot -- every credential in an installation is a reference to a secret by name, which is what makes installation describe safe to commit and makes this safe to send.
parameters.json Parameters and their values Values as well as names. A parameter is not a secret by construction -- its value already reaches Compose as an environment variable, docker inspect, status --json and the journal -- so withholding the values here would hide the most common cause of a support question while protecting nothing.
config-diff.txt Configuration drift Where the files on disk differ from what the release renders — the evidence an operator is usually asked for and least able to produce by hand. It cannot embed a secret: a configuration template is rendered with secret references, a name to the path of its rendered file, and the values never enter the render context.
journal.jsonl The operation journal Every operation this installation has run, newest first, with its steps and outcomes. It is the difference between "the update failed" and a timestamped account of which step failed and what it said.
doctor.json Diagnostic checks doctor's results as it would print them now, including the host facts its machine checks collect. Those facts have no producer separate from the checks that report them, so they are here rather than in a file of their own.
releases.json Version history Which releases are on this machine and which are current, previous and staged. "It worked before" needs a before.
services.json Service and health state What the runtime reports for each service right now. A bundle taken while something is down is worth more than one taken after a restart, and this is the part that says which it was.
manager.json Manager version and build Which binary produced this archive. It also identifies the redaction logic that ran, since the redactor ships with the manager and has no version of its own to state.
meta.json The archive's own account of itself What was collected, what was omitted and why, and the redaction count per file. A count of zero is not proof of cleanliness, but it is the first thing a reviewer looks at and it must not have to be inferred from the archive's shape.

Included after redaction

The only raw bytes from your containers in the archive, and the only component whose safety depends on the redactor rather than on its shape.

File Component Why
logs/ Container logs The single most useful component and the only raw vendor bytes in the archive. Bounded by lines and by bytes, passed through the same redactor morzer logs uses, and omitted entirely rather than included unfiltered when that redactor cannot be armed.

What redaction actually promises

Every component is scrubbed on its way into the archive, not only the one classified above. The class says where a component's bytes came from; redaction is about when they were written, and most of these were written long before you ran this command. The journal is the clear case: it is appended to across every operation this installation has ever run, and a message that embedded a secret was written at a moment the manager may not have known that secret yet.

meta.json records how many values were replaced in each file. A count of zero is not proof that a file was clean — it is proof that no value this installation currently holds appeared in it, which is a smaller claim. A secret that was rotated away, or one that was never declared to the manager at all, is not something it can recognise.

The journal has one more pass. A URL with a password in it, such as a bundle reference an older version journalled as typed, has the password taken out of every operation's flags, and each one is counted in the journal's number.

Container logs are the exception that is enforced rather than promised: if the secret values cannot be loaded, they are left out of the archive entirely and meta.json says so. Everything else still ships.

Before you send one

Run morzer support bundle with --preview. It writes nothing and prints the same component list with per-file sizes and redaction counts, so you can see what would leave this machine before anything does. It is the same table either way — one rendering, so the preview and the archive cannot disagree about what was collected.

--no-logs leaves your containers' output out. It is not a way to turn redaction off — it removes a component rather than removing the filter from it, so it can only ever send less. --dir writes the archive somewhere other than the working directory. There is no flag that disables redaction. A flag like that becomes the one every support article tells you to pass, and then redaction is a feature nobody uses; if you genuinely need unredacted output, morzer logs --no-redact makes that choice one file at a time.

Encrypting it to your vendor

A release can declare who support bundles are for. When it does, the archive is encrypted to those keys only — not to this machine — and gets an .age suffix.

$ morzer support bundle
10 component(s), 15.0 KiB
  FILE               COMPONENT                            SIZE     REDACTIONS
  manifest.yaml      The resolved manifest                2.8 KiB  0
  ...
  meta.json          The archive's own account of itself  1.4 KiB  0

  written
/home/op/support-demo-op_01M00V78YRZ392YXZY8001V127-20260814T191545Z.tar.zst.age

  encrypted to, and readable by, only these recipients
age1kyh0pf3u8hxepjrgy9k4zuv7tua0frkj6vsnn9jld7mspgx3qqkq6q34cq

That this machine cannot read it back is the point rather than an inconvenience. An archive in a ticket system, a mail thread or a bucket is then unreadable by the ticket system, the mail provider, and by anyone who later takes this host.

Your vendor declares it in the manifest, under the extensions namespace:

manifest.yaml
extensions:
  morzer.dev/support:
    recipients:
      - age1kyh0pf3u8hxepjrgy9k4zuv7tua0frkj6vsnn9jld7mspgx3qqkq6q34cq

It sits under extensions rather than at the top level so that a bundle declaring it still installs on a manager released before this existed. The cost is worth stating: such a manager ignores the block, so an operator who has not upgraded gets a plaintext archive — and is told so, in the same breath as the path, on every run.

A declaration that cannot be used is refused, and no archive is written. A missing recipients: list, an empty one, or a key that is not an age recipient all stop the command before anything is collected. Falling back to plaintext would hand an unencrypted archive to the operator who most clearly asked for an encrypted one, at the moment they are attaching it to a ticket.

Run --preview first and read the keys. The refusal catches a key that is malformed; nothing can catch one that is well-formed and belongs to the wrong party, so they are printed in full — compare them against what your vendor published, while the archive still does not exist.

Signing it, and what that proves

The archive is signed with this installation's own key, and the signature lands beside it as <archive>.minisig. It covers the file that leaves — for an encrypted archive that is the ciphertext, not the plaintext inside it — so whoever receives one can establish where it came from before handing it to anything that decrypts. It is also checkable with minisign -Vm <archive> -P <key>, the same gesture this project already teaches for a release.

What it proves is narrow, and the archive carries the sentence itself in meta.json so a reader handed the file with no documentation still gets it:

This signature proves that a process holding this installation's signing key produced these bytes. It does not prove the archive came from that machine -- a copied key signs from anywhere -- it does not prove nobody edited the contents before the archive was made, and it does not identify the operator. The key named above identifies which key to obtain from the installation's operator; it is not a key to verify against, because this file and that name were written by the same hand.

A machine that has never minted a signing key still writes the archive, unsigned, and says so on the run. Withholding evidence from the installation that has the least of it is the wrong failure.

Reading one back

morzer support inspect <file> lists what is in an archive and reports what its signature established. Nothing is extracted — the archive is read in memory — so inspecting an encrypted bundle leaves no readable copy on the machine doing the inspecting.

It is reading a file somebody else produced, so every read is bounded and anything past a bound is refused rather than truncated: the archive itself, the decrypted plaintext, the zstd window, and the detached signature, which is a few hundred bytes or is not a signature. Text the archive carries is stripped of control characters before it reaches your terminal.

The signature is checked before anything is decrypted, because it covers the file as it arrived. An encrypted archive you have no key for still reports its signature, and says that the contents could not be read rather than listing nothing.

$ morzer support inspect support-demo-op_01M032R1495VR14NRFX5NFX68T-20260815T160543Z.tar.zst
9 component(s), 14.2 KiB, plus the index (1.9 KiB)
  product       demo
  installation  op_01M032R1495VR14NRFX5NFX68T
  written by    0.0.0
  FILE               COMPONENT                    SIZE     REDACTIONS
  manifest.yaml      The resolved manifest        2.8 KiB  0
  ...
  NOT COLLECTED  WHY
  logs/          the deployment produced no log output to capture

  signature verified against this installation's recorded keys
RWQAjdUPyq2+wfrTz2Sea5r7DjoThqJQH69EfC+RnVHvBAchqC0iptOI

It counts one component fewer than bundle did, and the difference is meta.json: the index cannot list itself, so it is reported as the index rather than invented as a row with a redaction count nobody could know.

An encrypted archive needs --identity, holding a key your release named as a support recipient. The machine that produced the archive does not have one, on purpose, so it cannot read its own bundle back.

The key in the archive is a claim, not a check

meta.json names the key that signed the archive. That name proves nothing: whoever wrote the archive wrote the name beside the signature they made, and the two agree with each other perfectly.

So inspect never checks against it. It checks against one of three keys the archive's producer did not supply, which is the only property that matters:

  • this installation's recorded keys, when you run it on the machine that produced the archive;
  • --key, a key you got from the operator rather than from the file;
  • the signing key on this machine's disk, when installation state records none — which is the state of a machine that has signed without ever running init --repair. The verdict says this is what it used, because state did not back it.

With none of the three, it says so instead of printing a verdict it has not earned:

$ morzer support inspect bundle.tar.zst
  signature present and NOT checked: there was no key to check it against
  the archive says this key signed it -- get that key from the operator, not from this file, and
  pass it with --key
RWQAjdUPyq2+wfrTz2Sea5r7DjoThqJQH69EfC+RnVHvBAchqC0iptOI

$ morzer support inspect bundle.tar.zst --key RWQAjdUPyq2+wfrTz2Sea5r7DjoThqJQH69EfC+RnVHvBAchqC0iptOI
  signature verified against the key you named
RWQAjdUPyq2+wfrTz2Sea5r7DjoThqJQH69EfC+RnVHvBAchqC0iptOI

--key takes the key itself or a file holding one. A key this installation has retired is reported as its own outcome rather than as valid — it establishes where the archive came from, not that it is current.

Checking something you are sending by hand

morzer support redact with --check runs the same redactor over a file you were going to send anyway — a log you tailed into a file, a paste, a config you exported. It reports and writes nothing: the file is yours, and rewriting it would destroy what you were about to send.

$ morzer support redact --check /tmp/paste.txt
2 secret value(s) found in paste.txt
  do not send this file as it is

The archive is safe by construction. The thing you paste into a chat window is not, and this is the only part of the feature that can help with that.