feat(remote-signer): Turnkey ephemeral keystore for Railway - #85
feat(remote-signer): Turnkey ephemeral keystore for Railway#85eliteprox wants to merge 3 commits into
Conversation
Hosted signers cannot bind-mount a keystore. Export the Turnkey wallet at boot the same way pymthouse does, bind go-livepeer to Railway PORT, then wipe the UTC keystore once livepeer is ready.
There was a problem hiding this comment.
Pull request overview
This PR adds an optional Turnkey-backed bootstrap flow for the remote-signer so hosted deployments (Railway) can export a temporary UTC keystore at boot, bind to Railway’s injected PORT, wait for the signer endpoint to become ready, and then delete keystore artifacts. Local bind-mounted /data/keystore behavior remains unchanged when TURNKEY_* is unset.
Changes:
- Adds a new Go bootstrap binary (
signer-turnkey-bootstrap) that resolves/exports a Turnkey wallet account and writes a go-livepeer-compatible UTC keystore under/data. - Updates the remote-signer container entrypoint to support Railway
PORT, run the bootstrap in Turnkey mode, poll/sign-orchestrator-infofor readiness, and then delete ephemeral keystore artifacts. - Adds Railway deployment config, CI workflow coverage for the bootstrap module, and documentation/env templates for Turnkey mode.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| remote-signer/turnkey-bootstrap/main.go | Reads Turnkey/bootstrap env config and orchestrates account export + keystore write + resolved-address output. |
| remote-signer/turnkey-bootstrap/turnkey.go | Implements Turnkey client setup plus wallet/account resolution and export/decrypt flow. |
| remote-signer/turnkey-bootstrap/keystore.go | Parses decrypted private key payload formats and writes keystore + password + resolved address files. |
| remote-signer/turnkey-bootstrap/utc_keystore.go | Implements Web3 Secret Storage (V3) UTC keystore encryption compatible with go-livepeer/geth-style keystores. |
| remote-signer/turnkey-bootstrap/turnkey_test.go | Adds unit tests for derivation-path parsing and private-key payload parsing. |
| remote-signer/turnkey-bootstrap/go.mod | Introduces a standalone Go module for the bootstrap binary and its dependencies. |
| remote-signer/turnkey-bootstrap/go.sum | Locks bootstrap module dependency checksums. |
| remote-signer/entrypoint.sh | Adds Railway PORT support and Turnkey-mode bootstrap + readiness polling + keystore cleanup flow. |
| remote-signer/scripts/cleanup-ephemeral-keystore.sh | Adds a helper script to remove /data/keystore/* and /data/.eth-password. |
| remote-signer/Dockerfile | Builds and copies the bootstrap binary into the remote-signer image and ensures curl is present. |
| remote-signer/config/turnkey.env.example | Adds an env template for Turnkey mode variables. |
| deploy/remote-signer/railway.json | Adds Railway build/deploy configuration for the remote-signer service. |
| .github/workflows/remote-signer.yml | Adds CI for formatting/vet/build/test of the Turnkey bootstrap module when remote-signer paths change. |
| README.md | Documents Turnkey ephemeral keystore mode and Railway expectations. |
| .env.example | Adds commented Turnkey-related environment variables for hosted mode. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Forward SIGTERM/SIGINT to background livepeer, bound curl readiness probes, parse raw 32-byte keys without UTF-8 trimming, and round-trip the UTC keystore through go-ethereum DecryptKey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
remote-signer/turnkey-bootstrap/utc_keystore.go:175
ts := time.Now().UTC()forces the zone to UTC, so the subsequentts.Zone()check can never be non-UTC. This dead branch is misleading and can be removed without changing behavior.
ts := time.Now().UTC()
tz := "Z"
if name, offset := ts.Zone(); name != "UTC" {
tz = fmt.Sprintf("%03d00", offset/3600)
}
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (6)
remote-signer/entrypoint.sh:132
- If
livepeerexits before becoming ready, the entrypoint exits without any attempt to remove the exported keystore/password. In Turnkey mode this can leave credentials behind on/datawhen the process crashes on boot.
if ! kill -0 "$LIVEPEER_PID" 2>/dev/null; then
echo "entrypoint: livepeer (pid $LIVEPEER_PID) exited before becoming ready" >&2
wait "$LIVEPEER_PID" 2>/dev/null || true
exit 1
fi
remote-signer/entrypoint.sh:117
- On SIGTERM/SIGINT before the readiness+cleanup section completes, the trap exits after waiting for
livepeerbut does not clean up the exported keystore/password. That can leave secrets on the/datavolume if the container is stopped quickly after boot.
forward_signal() {
echo "entrypoint: forwarding $1 to livepeer (pid ${LIVEPEER_PID:-unset})" >&2
if [ -n "${LIVEPEER_PID:-}" ] && kill -0 "$LIVEPEER_PID" 2>/dev/null; then
kill -s "$1" "$LIVEPEER_PID" 2>/dev/null || true
wait "$LIVEPEER_PID"
status=$?
else
status=0
fi
trap - TERM INT
exit "$status"
}
remote-signer/scripts/cleanup-ephemeral-keystore.sh:4
- The cleanup glob
/data/keystore/*does not match dotfiles.writeUTCKeyFilecreates temp files with a leading.(e.g..UTC--...tmp), so a crash can leave a hidden file containing key material that this script will not delete.
rm -rf /data/keystore/* /data/.eth-password
remote-signer/entrypoint.sh:104
- In Turnkey mode, keystore/password cleanup only happens on the success path. On readiness timeout or early exit (including the existing
stop_livepeererror paths),/data/keystoreand/data/.eth-passwordcan be left behind on the attached volume, which undermines the “ephemeral” guarantee.
This issue also appears in the following locations of the same file:
- line 106
- line 128
stop_livepeer() {
if [ -n "${LIVEPEER_PID:-}" ] && kill -0 "$LIVEPEER_PID" 2>/dev/null; then
kill -s TERM "$LIVEPEER_PID" 2>/dev/null || true
wait "$LIVEPEER_PID" 2>/dev/null || true
fi
}
remote-signer/entrypoint.sh:27
TURNKEY_MODEis enabled based on the threeTURNKEY_*variables, but the bootstrap also requiresSIGNER_ETH_KEYSTORE_PASSWORDto be set (it may be empty). When users set TURNKEY vars but forget the password, this fails deep inside the bootstrap instead of being caught up-front in the entrypoint.
TURNKEY_MODE=0
if [ -n "${TURNKEY_ORG_ID:-}" ] && [ -n "${TURNKEY_API_PUBLIC_KEY:-}" ] && [ -n "${TURNKEY_API_PRIVATE_KEY:-}" ]; then
TURNKEY_MODE=1
fi
remote-signer/turnkey-bootstrap/utc_keystore.go:162
writeUTCKeyFileignores short writes (whereWritereturnsn < len(content)with a nil error). That’s rare but possible; for keystore files it’s safer to detect it and fail rather than writing a truncated JSON blob.
if _, err := tmp.Write(content); err != nil {
tmp.Close()
os.Remove(tmp.Name())
return err
}
Summary
PORT, waits until/sign-orchestrator-infois ready, then deletes/data/keystoreand.eth-password.deploy/remote-signer/railway.json. Local bind-mounted keystore behavior is unchanged whenTURNKEY_*is unset.Test plan
cd remote-signer/turnkey-bootstrap && go test ./... -count=1TURNKEY_*still starts fromremote-signer/data/TURNKEY_ORG_ID/ API keys /SIGNER_ETH_KEYSTORE_PASSWORD,KAFKA_BROKERS,REMOTE_SIGNER_WEBHOOK_URL, attach/datavolumesigner-turnkey-bootstrap: resolved signer addressthen keystore cleanupPOST /sign-orchestrator-infoon$PORTsucceeds; Kafkacreate_signed_ticketstill reaches the collector