Everything below runs on a connected staging machine first, then on the
disconnected side. Only the staging machine needs
registry access.
Mirror container images
On the connected machine, derive the image list from the chart you are about to install, so the list always matches the release:images.txt, then pull each image and give it the name it will have in
your registry. The references are pinned by digest, which docker save does not
accept as a name and which does not survive the transfer, so the rename happens
here, on the connected side:
zstd keeps the transfer archive small; gzip works too if zstd is not
available on both sides.
For the All-in-One image the same pattern applies with a single reference:
fish-audio-images.tar.zst, mirrored.txt, the chart archive from
./transfer/, and your values file across the air gap by whatever means your
policy allows.
Load on the disconnected side
mirrored.txt, and clear the pinned digest — the chart pins images by
digest, and a digest does not survive docker save, so leaving one in place
makes the pull fail against your own registry. Some components carry more than
one digest field; clear each of them. The bundled Redis splits its reference into
a registry and a repository field rather than one path, so set both.
Confirm nothing still refers to an external host before you install:
Model assets offline
No delivery form downloads model weights at runtime.
Two things still come from you rather than from the network:
- Reference voices. Stage the archives yourself, as described in Kubernetes deployment or All-in-One. This behavior is identical online and offline.
- Timestamp alignment. The forced aligner is not part of the offline bundle. Contact Fish Audio if your air-gapped deployment needs word or segment timings.
Offline usage accounting
With no billing service to call, the deployment records every charge to a local, signed, append-only ledger on persistent storage. No request is ever rejected for billing reasons, and the record is tamper-evident and independently verifiable.How tokens behave
- Any non-empty
Authorization: Bearertoken is accepted. Empty or missing still returns 401. - The token is recorded verbatim as the billing identity, so choose a stable, distinct value per tenant. Tenants that share a token cannot be told apart in the ledger.
Disk layout
Files are grouped one directory per UTC day, so a day archives by copying a single directory:Record format
Each line is one record: a shared envelope plus a payload chosen byevent_type
(process_started, heartbeat, billing_event, process_stopping).
Charges accumulate in Redis per token, product, and backend, and a background task
flushes them roughly once a minute as one aggregated
billing_event per bucket. A
failed write is retried rather than dropped.
When a file is finished it is sealed into a matching signed manifest carrying the
last record’s hash and signature, the record counts, and the file’s totals.
Sealing is automatic: yesterday’s file is sealed shortly after the UTC day rolls
over, and a graceful shutdown seals the current one. Today’s in-progress file has
no manifest yet.
Verify the ledger
Verification needs only the public key that Fish Audio supplies, plusjq and
openssl:
Verified OK confirms the record is authentic. Recompute the hash itself from the
record body and compare it to the record_hash field:
seq is contiguous, that each prev_hash
links the previous record, that the manifest signature verifies, and that the
manifest totals match the event file. Ask Fish Audio for the verification script
that runs all of these over a ledger directory.
The ledger is tamper-evident, not tamper-proof. Signature and hash-chain checks
reliably detect accidental corruption, and tampering by anyone holding the files
but not the image. They do not by themselves stop an operator who holds the image,
because the key that signs the records ships inside it. If your audit requirements call for
stronger guarantees, such as an independent write-once anchor or a separately
administered audit sink, raise it with Fish Audio so it can be designed into
the deployment.
Retention and reconciliation
- The ledger lives on persistent shared storage and survives restarts and rescheduling.
- Nothing prunes it. Archive completed day directories to your own storage on your retention schedule, and never delete the live directory out from under a running instance.
- Usage is reconciled afterwards from the signed day directories, on the cadence set in your agreement. Copy whole directories, including the manifests, so the totals can be verified independently.
Prove there is no egress
Configuration review is not proof. Demonstrate it.Kubernetes
Turn off Redis’s own allow-all egress first. The chart installs a NetworkPolicy for Redis that permits all outbound traffic, and NetworkPolicies are additive — a namespace-wide deny does not override it, so those pods stay unrestricted whatever else you apply:deny-external-egress.yaml
All-in-One
The strongest proof is a container that never had a network interface, on a fresh volume, so nothing could have been fetched even during the first compile:--network none no host port can be published, so drive the request from
inside the container once the cold start finishes:
model header
is optional. Its name is in the model registry inside the container at
/opt/edge-api/models.toml.
A 200 with a non-trivial audio file is the proof. A weaker but faster variant
disconnects an already-warmed container from every Docker network and repeats the
request; it demonstrates that the running service survives losing the network, but
not that the cold start never needed it.
Capture the output of whichever check you run as deployment evidence.
