Skip to main content

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:

  1. 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.
  2. 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 persistent

A 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:

ControlStops
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: restrictedPrivileged 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_LOCKAn 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-timeRoutine access to the process at all
A dedicated, tainted node pool and a ValidatingAdmissionPolicy refusing privileged, host-namespace or hostPath pods on itkubectl 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 pipelineSomeone else unsealing the seed with the Edge's identity

Three things this does not change, so they are written here rather than discovered:

  • A restricted debug container can still mount the pod's volumes. That is why the seed must be sealed: what it finds is ciphertext. In mnemonic mode 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:

ChainAddress
Bitcoinbc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu
Ethereum0x9858EfFD232B4033E47d90003D41EC34EcaEda94

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.