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 and pack the images. zstd keeps the transfer
archive small; gzip works too if zstd is not available on both sides.
fish-audio-images.tar.zst, images.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
image.repository in your values file, and confirm nothing still refers to an
external host before you install:
Model assets offline
The offline forms never download 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: signature and hash-chain checks reliably detect
corruption and modification of the files. 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
Deny external egress for the namespace, keeping in-cluster traffic and DNS, then confirm the deployment still generates audio.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:
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.
