Installing the Edge
One static Go binary in a scratch image. No runtime, no sidecar, no inbound port.
The console generates a Compose file with your API key already filled in — Settings → Edge readiness, while the indicators are still red. This page is the same thing with the reasoning attached, plus the Kubernetes form.
Before you start
You need two things:
- A BIP-39 mnemonic, generated offline, backed up, in a file at mode
0400. Read Mnemonic management first — it is the one step with no undo. - An API key from Settings → API keys in the console. Minting requires a fresh
TOTP code. The prefix decides the environment:
flk_test_*for sandbox,flk_live_*for live.
Docker Compose
services:
chainos-edge:
image: ziklag/chainos-edge:1.0
# Loopback only. The Edge holds your key material and its local API is
# authenticated by keys it issues itself, but nothing outside this host needs it.
ports:
- "127.0.0.1:8787:8787"
environment:
PASS_PHRASE_FILE: /run/secrets/chainos_mnemonic
API_KEY: ${CHAINOS_API_KEY}
CHAINOS_CLOUD_URL: https://api.chainos.cloud
secrets:
- chainos_mnemonic
volumes:
- edge_data:/var/lib/chainos
restart: unless-stopped
healthcheck:
test: ["CMD", "/chainos-edge", "healthcheck"]
interval: 30s
timeout: 3s
retries: 3
secrets:
chainos_mnemonic:
file: ./secrets/mnemonic.txt # chmod 0400
volumes:
edge_data:
docker compose up -d
docker compose logs -f chainos-edge
A first boot logs synced with Cloud and a line about derived Solana and Stellar pool
addresses. Every boot after that logs resumed with Cloud and the pool lines are absent —
enrolment is a one-off, and a restart carrying it again would be waste that compounds badly
under an autoscaler.
Within about thirty seconds the readiness panel in the console turns green.
With a sealed seed
The same service with the phrase sealed under an AWS KMS key
(Keystores). The envelope is ciphertext, so it is
an ordinary file; there is no PASS_PHRASE* at all, and the Edge refuses to start if one is set.
services:
chainos-edge:
image: ziklag/chainos-edge:1.0
ports:
- "127.0.0.1:8787:8787"
environment:
EDGE_KEYSTORE: awskms
PASS_PHRASE_SEALED_FILE: /run/chainos/edge-seed.sealed.json
EDGE_AWSKMS_KEY_ID: arn:aws:kms:eu-west-1:123456789012:key/…
AWS_REGION: eu-west-1
API_KEY: ${CHAINOS_API_KEY}
CHAINOS_CLOUD_URL: https://api.chainos.cloud
volumes:
- ./edge-seed.sealed.json:/run/chainos/edge-seed.sealed.json:ro
- edge_data:/var/lib/chainos
restart: unless-stopped
On EC2 the credentials come from the instance profile. A container on Docker's bridge network
reaches the instance metadata service one network hop further than the host, so raise the
instance's IMDSv2 hop limit to 2 (aws ec2 modify-instance-metadata-options --http-put-response-hop-limit 2)
or the Edge finds no credentials and refuses to start. Do not put AWS_ACCESS_KEY_ID in the
environment instead — the Edge refuses that too.
Kubernetes
apiVersion: apps/v1
kind: StatefulSet # not a Deployment — see the storage note below
metadata:
name: chainos-edge
spec:
serviceName: chainos-edge
replicas: 1
selector:
matchLabels: { app: chainos-edge }
template:
metadata:
labels: { app: chainos-edge }
spec:
securityContext:
# Lets the container's group read the 0400 secret file, which is
# otherwise owned by root and unreadable to a non-root uid.
fsGroup: 10001
containers:
- name: edge
image: ziklag/chainos-edge:1.0
ports:
- containerPort: 8787
env:
- name: PASS_PHRASE_FILE
value: /run/secrets/chainos/mnemonic
- name: API_KEY
valueFrom:
secretKeyRef: { name: chainos-api-key, key: apiKey }
- name: CHAINOS_CLOUD_URL
value: https://api.chainos.cloud
- name: EDGE_POD_HINT
valueFrom:
fieldRef: { fieldPath: metadata.name }
volumeMounts:
- name: mnemonic
mountPath: /run/secrets/chainos
readOnly: true
- name: data
mountPath: /var/lib/chainos
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
seccompProfile: { type: RuntimeDefault }
capabilities:
# No capability is needed. The Edge locks only the few KiB that hold
# the seed, which the default RLIMIT_MEMLOCK covers, so this runs
# under Pod Security "restricted".
drop: ["ALL"]
livenessProbe:
httpGet: { path: /v1/edge/health, port: 8787 }
periodSeconds: 30
readinessProbe:
httpGet: { path: /v1/edge/health, port: 8787 }
periodSeconds: 10
volumes:
- name: mnemonic
secret:
secretName: chainos-mnemonic
defaultMode: 0400
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 1Gi } }
A ClusterIP Service in front of it, reachable only from your application's namespace. No
Ingress — there is nothing to expose.
EDGE_DATA_DIR must be persistentA StatefulSet with a volumeClaimTemplate, not a Deployment with an emptyDir.
The Edge refuses to start if its data directory is on a tmpfs or an emptyDir, because
the outbox there would silently lose its contents on every restart while continuing to look
durable — which is worse than having no outbox at all. If you genuinely accept that loss,
set EDGE_OUTBOX_MODE=disabled explicitly and read
the outbox section for what else that costs.
Kubernetes, hardened
The manifest above is the shortest one that works. For production, start from the reference
set in the repository at
chainos-edge/deploy/kubernetes/,
which CI applies to a real cluster on every change and checks claim by claim. What it does,
and what each part stops:
| Control | Stops |
|---|---|
A sealed keystore: the envelope in a ConfigMap, no PASS_PHRASE* | Anyone reading Secrets, volume snapshots or etcd backups getting the phrase |
Namespace labelled pod-security.kubernetes.io/enforce: restricted | Privileged and SYS_PTRACE debug containers — admission rejects them, ephemeral containers included |
The Edge non-dumpable, capabilities.drop: [ALL], RuntimeDefault seccomp, read-only root, no IPC_LOCK | An ordinary debug container reading /proc/<pid>/environ, /proc/<pid>/mem or the Edge's mounts |
A deployer Role without pods/exec, pods/attach, pods/portforward, pods/ephemeralcontainers or secrets; a separate break-glass Role bound just-in-time | Routine access to the process at all |
A dedicated, tainted node pool and a ValidatingAdmissionPolicy refusing privileged, host-namespace or hostPath pods on it | kubectl debug node/… and any privileged pod on the Edge's nodes — the bypass of everything above |
| A ServiceAccount bound to the only cloud role that may decrypt, with pod creation in the namespace limited to the deploy pipeline | Someone else unsealing the seed with the Edge's identity |
Three things this does not change, so they are written here rather than discovered:
- A
restricteddebug container can still mount the pod's volumes. That is why the seed must be sealed: what it finds is ciphertext. Inmnemonicmode it finds the phrase. - Anyone who can run a pod as the Edge's ServiceAccount can decrypt the seed, the same way the Edge does. Limit who can create pods in the namespace; attested keystores (planned) close this.
- Root on the node, or the hypervisor, can read the Edge's memory. The node pool and the admission policy narrow who that is. They do not make it nobody.
The ServiceAccount carries no Kubernetes API token (automountServiceAccountToken: false).
EKS Pod Identity, IRSA and AKS Workload Identity inject their own projected tokens; the
comments in serviceaccount.yaml show where each binding goes.
Verify it yourself
Check the Edge before you check the console. Before the first sync the console can tell you nothing useful — Cloud has no extended public keys, so every chain is red with "No Edge has connected yet" — and the actual reason lives on the Edge.
curl -s http://localhost:8787/v1/edge/health
There is also a status page at http://localhost:8787/ in a browser, embedded in the
binary and served before any sync has happened.
Then confirm the derivation is right. For the published BIP-39 test vector the first addresses are:
| Chain | Address |
|---|---|
| Bitcoin | bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu |
| Ethereum | 0x9858EfFD232B4033E47d90003D41EC34EcaEda94 |
If either differs, stop — derivation has regressed and nothing else matters.
Mint a key for your application
Your banking application never holds a Ziklag credential.
curl -s -X POST http://localhost:8787/v1/edge/keys \
-H "Content-Type: application/json" \
-d '{ "label": "core-banking-prod" }'
The key is returned once. It is revocable individually
(DELETE /v1/edge/keys/{id}) and propagates to every Edge replica within one heartbeat.
Upgrading
The Edge is stateless apart from its data directory, and its enrolment survives a version change:
docker compose pull chainos-edge && docker compose up -d chainos-edge
Check the minimum supported Edge version in the console before jumping several releases — Cloud reports it in the portal configuration and will refuse a version below it.
If it does not come up
Troubleshooting covers the failures in order of how often they happen. The three most common are a mnemonic file the container cannot read, a data directory on ephemeral storage, and an API key whose environment does not match the one Cloud has recorded.