Skip to content

feat(remote-signer): Turnkey ephemeral keystore for Railway - #85

Draft
eliteprox wants to merge 3 commits into
mainfrom
feat/remote-signer-turnkey
Draft

feat(remote-signer): Turnkey ephemeral keystore for Railway#85
eliteprox wants to merge 3 commits into
mainfrom
feat/remote-signer-turnkey

Conversation

@eliteprox

Copy link
Copy Markdown
Collaborator

Summary

  • Adds pymthouse-style Turnkey bootstrap so a hosted remote signer can export an ephemeral UTC keystore at boot instead of bind-mounting keys.
  • Binds go-livepeer to Railway PORT, waits until /sign-orchestrator-info is ready, then deletes /data/keystore and .eth-password.
  • Railway service config lives at deploy/remote-signer/railway.json. Local bind-mounted keystore behavior is unchanged when TURNKEY_* is unset.

Test plan

  • cd remote-signer/turnkey-bootstrap && go test ./... -count=1
  • Local Compose without TURNKEY_* still starts from remote-signer/data/
  • Railway: set TURNKEY_ORG_ID / API keys / SIGNER_ETH_KEYSTORE_PASSWORD, KAFKA_BROKERS, REMOTE_SIGNER_WEBHOOK_URL, attach /data volume
  • Confirm logs show signer-turnkey-bootstrap: resolved signer address then keystore cleanup
  • POST /sign-orchestrator-info on $PORT succeeds; Kafka create_signed_ticket still reaches the collector

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.
Copilot AI lite review requested due to automatic review settings August 13, 2026 03:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-info for 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.

Comment thread remote-signer/entrypoint.sh
Comment thread remote-signer/entrypoint.sh Outdated
Comment thread remote-signer/turnkey-bootstrap/keystore.go
Comment thread remote-signer/turnkey-bootstrap/utc_keystore.go
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.
Copilot AI review requested due to automatic review settings August 13, 2026 03:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 subsequent ts.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)
	}

Comment thread remote-signer/turnkey-bootstrap/go.mod Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 13, 2026 13:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 livepeer exits before becoming ready, the entrypoint exits without any attempt to remove the exported keystore/password. In Turnkey mode this can leave credentials behind on /data when 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 livepeer but does not clean up the exported keystore/password. That can leave secrets on the /data volume 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. writeUTCKeyFile creates 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_livepeer error paths), /data/keystore and /data/.eth-password can 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_MODE is enabled based on the three TURNKEY_* variables, but the bootstrap also requires SIGNER_ETH_KEYSTORE_PASSWORD to 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

  • writeUTCKeyFile ignores short writes (where Write returns n < 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
	}

@eliteprox
eliteprox marked this pull request as draft August 17, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants