Skip to content

feat(nodes): opt-in encryption at rest for the outbound node API token - #6186

Open
n0ctal wants to merge 1 commit into
MHSanaei:mainfrom
n0ctal:upstream-node-token-encryption
Open

feat(nodes): opt-in encryption at rest for the outbound node API token#6186
n0ctal wants to merge 1 commit into
MHSanaei:mainfrom
n0ctal:upstream-node-token-encryption

Conversation

@n0ctal

@n0ctal n0ctal commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Add opt-in encryption at rest for the outbound node API token, with an explicit three-state policy (off / migration / required) and a rotatable keyring. Default is off, which is today's behaviour byte for byte.

Like the port-reservation PR, this is a proposal — it adds a package and a key file, so I would rather agree the shape than land it unannounced.

Why

nodes.api_token is the bearer token the panel presents to each worker. It is stored in plaintext, so anyone who reads the database reads a live credential for every node: a database backup, a copied SQLite file, a support dump, a restore onto a machine that should not have had it, an operator with read access to the panel database but not to the nodes.

The token is not derivable, expiring, or scoped — possession is sufficient to drive the node's API. That makes the panel database a single artefact whose disclosure compromises every node behind it, and database copies move around far more casually than node credentials do.

Scope

  • New internal/crypto/nodetoken: AEAD encrypt/decrypt bound to the node id, plus a Keyring with key ids so a token can be re-encrypted under a new key without a flag day.
  • KeySource with two implementations: a mode-0600 JSON key file (/etc/x-ui/node_token_key.json, override with XUI_NODE_TOKEN_KEY_FILE) and a single-key environment variable (XUI_NODE_TOKEN_KEY). The key is never accepted as a command-line argument, where it would be visible in ps.
  • NODE_TOKEN_ENCRYPTION policy:
    • off (default) — plaintext, exactly as today.
    • migration — read plaintext and ciphertext, always write ciphertext. This is the path onto encryption without downtime.
    • required — the key must load or startup fails.
  • The policy is explicit on purpose: a deployment that has already encrypted its tokens must not silently fall back to plaintext because a key file went missing. That failure should be loud.
  • node.go encrypts on write and decrypts on read inside the existing transactions; a plaintext row passes through unchanged so a partially migrated table keeps working.

Validation

  • go build ./internal/... clean.
  • go test ./internal/crypto/nodetoken/ ./internal/web/service/ green on main at ece16559.
  • Tests cover round-trip, node-id binding (a token encrypted for one node does not decrypt for another), key rotation across ids, malformed keys, and all three policy states including the mixed plaintext/ciphertext table that migration has to tolerate.

Risk

None for existing installs: with NODE_TOKEN_ENCRYPTION unset the code path is the current one.

The operational cost is real and worth stating — enabling this means a key file that has to survive alongside the database, and losing it while in required mode means the panel will not start until it is restored or the tokens are reissued. That is a deliberate trade: I would rather a lost key stop the panel than quietly return it to plaintext. If you would prefer a different default or a simpler single-key form without the keyring, I am happy to cut it down.

@n0ctal
n0ctal force-pushed the upstream-node-token-encryption branch from d74b71b to 166c9d4 Compare August 8, 2026 02:15
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.

1 participant