Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 44 additions & 27 deletions compose.individual-services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,36 +37,58 @@ services:
POSTGRES_PASSWORD: password
POSTGRES_DB: rollupsdb

# NOTE: compose can't override uid/gid/mode for secrets, it bind mounts them
# with the host credentials. The portable workaround to deal with this was to
# use volumes instead and manually chown/chmod the files upon creation.
secret-init:
image: cartesi/rollups-node:devel
user: root # required by: chown
entrypoint:
- sh
- -c
- |
set -e
cp /src/* /dst/
chown 102:102 /dst /dst/*
chmod 0400 /dst/*
volumes:
- ./test/secrets:/src:ro
- node_secrets:/dst
network_mode: "none"
restart: "no"

migration:
image: cartesi/rollups-node:devel
command: cartesi-rollups-cli db init
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
volumes:
- node_secrets:/run/secrets:ro
networks:
- devnet
restart: "no"
secrets:
- database_connection
environment:
<<: *env

evmreader:
image: cartesi/rollups-node:devel
command: cartesi-rollups-evm-reader
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
ethereum_provider:
condition: service_healthy
volumes:
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10001:10001" # Supervisor
secrets:
- auth_mnemonic
- blockchain_http_endpoint
- database_connection
environment:
<<: *env

Expand All @@ -75,84 +97,79 @@ services:
init: true
command: cartesi-rollups-advancer
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
ethereum_provider:
condition: service_healthy
volumes:
- node_data:/var/lib/cartesi-rollups-node/data
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10002:10002" # Supervisor
- "10012:10012" # Inspect Service
secrets:
- auth_mnemonic
- database_connection
environment:
<<: *env

validator:
image: cartesi/rollups-node:devel
command: cartesi-rollups-validator
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
volumes:
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10003:10003" # Supervisor
secrets:
- database_connection
environment:
<<: *env

claimer:
image: cartesi/rollups-node:devel
command: cartesi-rollups-claimer
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
ethereum_provider:
condition: service_healthy
volumes:
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10004:10004" # Supervisor
secrets:
- auth_mnemonic
- blockchain_http_endpoint
- database_connection
environment:
<<: *env

jsonrpc-api:
image: cartesi/rollups-node:devel
command: cartesi-rollups-jsonrpc-api
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
volumes:
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10005:10005" # Supervisor
- "10011:10011" # Jsonrpc API service
secrets:
- auth_mnemonic
- blockchain_http_endpoint
- database_connection
environment:
<<: *env
volumes:
node_data:
node_secrets:

networks:
devnet:

secrets:
auth_mnemonic:
file: test/secrets/auth_mnemonic.txt
blockchain_http_endpoint:
file: test/secrets/blockchain_http_endpoint.txt
database_connection:
file: test/secrets/database_connection.txt
42 changes: 28 additions & 14 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,38 @@ services:
POSTGRES_PASSWORD: password
POSTGRES_DB: rollupsdb

# NOTE: compose can't override uid/gid/mode for secrets, it bind mounts them
# with the host credentials. The portable workaround to deal with this was to
# use volumes instead and manually chown/chmod the files upon creation.
secret-init:
image: cartesi/rollups-node:devel
user: root # required by: chown
entrypoint:
- sh
- -c
- |
set -e
cp /src/* /dst/
chown 102:102 /dst /dst/*
chmod 0400 /dst/*
volumes:
- ./test/secrets:/src:ro
- node_secrets:/dst
network_mode: "none"
restart: "no"

migration:
image: cartesi/rollups-node:devel
command: cartesi-rollups-cli db init
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
networks:
- devnet
secrets:
- database_connection
volumes:
- node_secrets:/run/secrets:ro
restart: "no"
environment:
<<: *env
Expand All @@ -56,35 +78,27 @@ services:
init: true
command: cartesi-rollups-node
depends_on:
secret-init:
condition: service_completed_successfully
database:
condition: service_healthy
ethereum_provider:
condition: service_healthy
volumes:
- node_data:/var/lib/cartesi-rollups-node/data
- node_secrets:/run/secrets:ro
networks:
- devnet
ports:
- "10000:10000" # Supervisor
- "10011:10011" # Jsonrpc API service
- "10012:10012" # Inspect Service
secrets:
- auth_mnemonic
- blockchain_http_endpoint
- database_connection
environment:
<<: *env

volumes:
node_data:
node_secrets:

networks:
devnet:

secrets:
auth_mnemonic:
file: test/secrets/auth_mnemonic.txt
blockchain_http_endpoint:
file: test/secrets/blockchain_http_endpoint.txt
database_connection:
file: test/secrets/database_connection.txt
106 changes: 106 additions & 0 deletions docs/secrets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Secret File Handling

**Applies to:** deployments that pass credentials to the node via `*_FILE`
variables (database credentials, RPC endpoints, signing keys, API tokens),
on Kubernetes or Docker Compose.

## Node expectations

Every value loaded from a `*_FILE` variable is validated at startup, before
use; the node refuses to start on violation. The path must resolve to a
**regular file** — symlinks, directories, FIFOs, and device nodes are
rejected.

Secrets must match one of two canonical forms tailored to common deployment scenarios:

| Deployment | Ownership | Mode |
| ----------------- | --------------------------------- | -----------------|
| Kubernetes Secret | `root:fsGroup` (the node's group) | `0440` |
| Compose / host | the node's user | `0400` or `0600` |

Anything else is rejected: world bits, `0440` with a non-root owner, `0440`
whose group is not the node's, and `0400`/`0600` owned by another user. The
owner write bit (`0600`) is accepted in the Compose form so the owner can
rotate the file in place.

Affected variables:

- `CARTESI_AUTH_MNEMONIC_FILE`,
- `CARTESI_AUTH_PRIVATE_KEY_FILE`,
- `CARTESI_BLOCKCHAIN_HTTP_AUTHORIZATION_FILE`,
- `CARTESI_DATABASE_CONNECTION_FILE`,
- `CARTESI_BLOCKCHAIN_HTTP_ENDPOINT_FILE`.

Errors name the file but never its contents:

```text
failed to parse CARTESI_AUTH_MNEMONIC_FILE: secret file "/run/secrets/auth_mnemonic" does not conform with uid/gid/mode rules
```

On non-POSIX platforms only the regular-file check is enforced.

## Kubernetes

Mount the `Secret` as a read-only volume (not env vars), with
`defaultMode: 0440` and a Pod `fsGroup` equal to the node's GID: the
kubelet creates the files root-owned with that group, which is exactly the
canonical form above. `fsGroup` must equal the node user's primary GID
(102 in the stock image) and `runAsUser` must be the node's UID (102).
The check compares the file's group against the process's primary GID,
not its supplementary groups, so a different `fsGroup` (e.g. 65534) is
rejected even though Kubernetes makes the file readable.

## Docker Compose

Compose ignores `uid`/`gid`/`mode` on file-based secrets (it warns and
discards them): the in-container ownership and mode are exactly the **host
file's** numeric UID/GID and mode. The node's startup validation is the only
enforcement point, so the file as seen from the container must already match
the Compose canonical form.

The straightforward workaround is to fix the host file itself — `chown
102:102` and `chmod 0400` (or `0600`).

An option that never modifies host files is a one-shot `secret-init`
service: run as root (only to write the fresh root-owned volume), it copies
the files into a named volume with canonical `102:102` / `0400` identity,
and the node mounts that volume read-only at `/run/secrets`. Host file
ownership and modes do not matter — the copy is normalized; the copy
re-runs on every `up`; `down -v` wipes the volume. `compose.yaml`
implements this pattern:

```yaml
services:
secret-init:
image: cartesi/rollups-node:devel
user: root # required by: chown
entrypoint:
- sh
- -c
- |
set -e
cp /src/* /dst/
chown 102:102 /dst /dst/*
chmod 0400 /dst/*
volumes:
- ./test/secrets:/src:ro
- node_secrets:/dst
network_mode: "none"
restart: "no"

node:
depends_on:
secret-init:
condition: service_completed_successfully
volumes:
- node_secrets:/run/secrets:ro

volumes:
node_secrets:
```

## Checklist

- [ ] `*_FILE` secret files match a canonical form: `0400`/`0600` node-owned (Compose) or `0440` root:fsGroup (Kubernetes)
- [ ] Kubernetes: `defaultMode: 0440`, `fsGroup` = node user's primary GID (102), `runAsUser` = node's UID (102)
- [ ] Compose: host files `chown 102:102` / `chmod 0400` (YAML `uid`/`gid`/`mode` are ignored)
3 changes: 1 addition & 2 deletions internal/config/generate/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ package config

import (
"fmt"
"os"
"strings"

"github.com/spf13/viper"
Expand Down Expand Up @@ -221,7 +220,7 @@ func Get{{ toFieldName .Name }}() ({{ .GoType }}, error) {
{{- if .File }}
if s == "" {
filename := viper.GetString({{toConstName .Name}}_FILE)
contents, err := os.ReadFile(filename)
contents, err := ReadSecretFile(filename)
if err != nil {
return notDefined{{ .GoType }}(), fmt.Errorf("failed to parse %s: %w", {{ toConstName .Name }}_FILE, err)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/config/generate/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ This file documents the configuration options.
{{- if .Default}}
* **Default:** {{.Default | quote | backtick}}
{{- end}}
{{- if .File}}
* **File variant:** {{printf "%s_FILE" .Name | backtick}} reads the value from a file.
The file must be a regular file matching one of the canonical forms:
{{backtick "0400"}} or {{backtick "0600"}} owned by the node's user (Compose / host files), or
{{backtick "0440"}} owned by root with the node's group (Kubernetes {{backtick "fsGroup"}}).
Symlinks, directories, FIFOs, and device nodes are rejected. The node
refuses to start otherwise.
{{- end}}
{{- if .UsedBy}}
* **Used by:** {{range $i, $e := .UsedBy}}{{if $i}}, {{end}}{{$e}}{{end}}
{{- end}}
Expand Down
Loading