Skip to content

Commit 53ec293

Browse files
committed
feat(config): Hardening of secret file handling
1 parent d5a6e17 commit 53ec293

9 files changed

Lines changed: 330 additions & 21 deletions

File tree

compose.individual-services.yaml

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ services:
4747
- devnet
4848
restart: "no"
4949
secrets:
50-
- database_connection
50+
- source: database_connection
51+
target: database_connection
52+
uid: "102"
53+
gid: "102"
54+
mode: 0440
5155
environment:
5256
<<: *env
5357

@@ -64,9 +68,17 @@ services:
6468
ports:
6569
- "10001:10001" # Supervisor
6670
secrets:
67-
- auth_mnemonic
71+
- source: auth_mnemonic
72+
target: auth_mnemonic
73+
uid: "102"
74+
gid: "102"
75+
mode: 0400
6876
- blockchain_http_endpoint
69-
- database_connection
77+
- source: database_connection
78+
target: database_connection
79+
uid: "102"
80+
gid: "102"
81+
mode: 0440
7082
environment:
7183
<<: *env
7284

@@ -87,8 +99,16 @@ services:
8799
- "10002:10002" # Supervisor
88100
- "10012:10012" # Inspect Service
89101
secrets:
90-
- auth_mnemonic
91-
- database_connection
102+
- source: auth_mnemonic
103+
target: auth_mnemonic
104+
uid: "102"
105+
gid: "102"
106+
mode: 0400
107+
- source: database_connection
108+
target: database_connection
109+
uid: "102"
110+
gid: "102"
111+
mode: 0440
92112
environment:
93113
<<: *env
94114

@@ -103,7 +123,11 @@ services:
103123
ports:
104124
- "10003:10003" # Supervisor
105125
secrets:
106-
- database_connection
126+
- source: database_connection
127+
target: database_connection
128+
uid: "102"
129+
gid: "102"
130+
mode: 0440
107131
environment:
108132
<<: *env
109133

@@ -120,9 +144,17 @@ services:
120144
ports:
121145
- "10004:10004" # Supervisor
122146
secrets:
123-
- auth_mnemonic
147+
- source: auth_mnemonic
148+
target: auth_mnemonic
149+
uid: "102"
150+
gid: "102"
151+
mode: 0400
124152
- blockchain_http_endpoint
125-
- database_connection
153+
- source: database_connection
154+
target: database_connection
155+
uid: "102"
156+
gid: "102"
157+
mode: 0440
126158
environment:
127159
<<: *env
128160

@@ -138,9 +170,17 @@ services:
138170
- "10005:10005" # Supervisor
139171
- "10011:10011" # Jsonrpc API service
140172
secrets:
141-
- auth_mnemonic
173+
- source: auth_mnemonic
174+
target: auth_mnemonic
175+
uid: "102"
176+
gid: "102"
177+
mode: 0400
142178
- blockchain_http_endpoint
143-
- database_connection
179+
- source: database_connection
180+
target: database_connection
181+
uid: "102"
182+
gid: "102"
183+
mode: 0440
144184
environment:
145185
<<: *env
146186
volumes:

compose.yaml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ services:
3333
interval: 3s
3434
timeout: 3s
3535
retries: 5
36+
secrets:
37+
- source: database_connection
38+
target: database_connection
39+
uid: "102"
40+
gid: "102"
41+
mode: 0440
3642
environment:
3743
POSTGRES_PASSWORD: password
3844
POSTGRES_DB: rollupsdb
@@ -46,7 +52,11 @@ services:
4652
networks:
4753
- devnet
4854
secrets:
49-
- database_connection
55+
- source: database_connection
56+
target: database_connection
57+
uid: "102"
58+
gid: "102"
59+
mode: 0440
5060
restart: "no"
5161
environment:
5262
<<: *env
@@ -69,9 +79,17 @@ services:
6979
- "10011:10011" # Jsonrpc API service
7080
- "10012:10012" # Inspect Service
7181
secrets:
72-
- auth_mnemonic
82+
- source: auth_mnemonic
83+
target: auth_mnemonic
84+
uid: "102"
85+
gid: "102"
86+
mode: 0400
7387
- blockchain_http_endpoint
74-
- database_connection
88+
- source: database_connection
89+
target: database_connection
90+
uid: "102"
91+
gid: "102"
92+
mode: 0440
7593
environment:
7694
<<: *env
7795

docs/secret-files.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Secret File Permissions
2+
3+
The node reads several sensitive configuration values from files via the
4+
`*_FILE` environment variables. To avoid silently accepting insecure secret
5+
mounts, the node validates the file **type and permissions** before reading
6+
the contents and rejects files that violate the policy.
7+
8+
This applies to the file-backed variants of:
9+
10+
| Variable | Policy | Sensitivity |
11+
| ------------------------------------------- | ------------- | ---------------------------------------- |
12+
| `CARTESI_AUTH_MNEMONIC_FILE` | strict-secret | signing material (mnemonic) |
13+
| `CARTESI_AUTH_PRIVATE_KEY_FILE` | strict-secret | signing material (private key) |
14+
| `CARTESI_BLOCKCHAIN_HTTP_AUTHORIZATION_FILE`| credential | outbound authorization header |
15+
| `CARTESI_DATABASE_CONNECTION_FILE` | credential | database connection string (DSN) |
16+
| `CARTESI_BLOCKCHAIN_HTTP_ENDPOINT_FILE` | regular-file | endpoint without embedded credentials |
17+
18+
## Policy tiers
19+
20+
### strict-secret (signing material)
21+
22+
Applies to the mnemonic and private-key files.
23+
24+
The file:
25+
26+
- **must be a regular file** (not a directory, device, pipe, etc.);
27+
- **must not be readable, writable, or executable by group or world**
28+
(`mode & 0o077` must be `0`, i.e. owner-only, such as `0400` or `0600`);
29+
- **must be owned by the current effective user** on POSIX systems.
30+
31+
On violation the node refuses to start. This is a hard failure, not a warning.
32+
33+
### credential (authorization headers, DSNs)
34+
35+
Applies to files carrying passwords or authorization tokens.
36+
37+
The file:
38+
39+
- **must be a regular file**;
40+
- **must not be world-readable or world-writable**
41+
(`mode & 0o007` must be `0`).
42+
43+
Group-readable files are accepted (e.g. `0640`), so a shared group can mount
44+
credentials when needed.
45+
46+
### regular-file (non-secret endpoint files)
47+
48+
Applies to endpoint files that do not carry credentials.
49+
50+
The file:
51+
52+
- **must be a regular file**.
53+
54+
No permission restrictions beyond that. This tier keeps URL convenience files
55+
from being conflated with private key material.
56+
57+
## Non-POSIX platforms
58+
59+
On Windows and other platforms that do not expose POSIX mode semantics, only the
60+
regular-file check is enforced; mode and ownership checks are skipped. Treat
61+
this as best-effort hardening, not cross-platform parity.
62+
63+
## Required file modes
64+
65+
When mounting secrets into the container, use owner-only permissions for signing
66+
material:
67+
68+
```sh
69+
chmod 600 /run/secrets/auth_mnemonic
70+
chmod 600 /run/secrets/auth_private_key
71+
```
72+
73+
and group-only or owner-only permissions for credential files:
74+
75+
```sh
76+
chmod 640 /run/secrets/blockchain_http_authorization
77+
chmod 640 /run/secrets/database_connection
78+
```
79+
80+
The node runs as a non-root user (`cartesi`, uid 102, gid 102). Secret files must
81+
be readable by that user.
82+
83+
## Docker Compose
84+
85+
Docker Compose mounts secrets into `/run/secrets/<name>`. By default they are
86+
**owned by `root`** and **world-readable (`0444`)**, which the `strict-secret`
87+
policy rejects. Mount signing secrets with an explicit mode and ownership so
88+
they are readable only by the node user, for example:
89+
90+
```yaml
91+
services:
92+
node:
93+
secrets:
94+
- source: auth_mnemonic
95+
target: auth_mnemonic
96+
uid: "102"
97+
gid: "102"
98+
mode: 0400
99+
100+
secrets:
101+
auth_mnemonic:
102+
file: test/secrets/auth_mnemonic.txt
103+
```
104+
105+
> Note: the `uid`, `gid`, and `mode` fields on service secrets require a recent
106+
> Docker Compose version. Verify support in your environment; if they are
107+
> unsupported, mount the secret as a bind volume with the host file owned by uid
108+
> 102 and mode `0400` instead.
109+
110+
The node rejects insecure secret files at startup with a message such as:
111+
112+
```text
113+
failed to parse CARTESI_AUTH_MNEMONIC_FILE: file "/run/secrets/auth_mnemonic"
114+
is accessible by group or others; expected owner-only permissions
115+
```
116+
117+
Error messages never include the file contents.

internal/config/generate/Config.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,15 @@ used-by = ["evmreader", "claimer", "node", "prt"]
184184

185185
[blockchain.CARTESI_BLOCKCHAIN_HTTP_ENDPOINT]
186186
file = true
187+
file-policy = "regular-file"
187188
go-type = "URL"
188189
description = """
189190
HTTP endpoint for the blockchain RPC provider."""
190191
used-by = ["evmreader", "claimer", "node", "prt"]
191192

192193
[blockchain.CARTESI_BLOCKCHAIN_HTTP_AUTHORIZATION]
193194
file = true
195+
file-policy = "credential"
194196
go-type = "RedactedString"
195197
description = """
196198
Additional security when interacting with providers.
@@ -334,6 +336,7 @@ used-by = ["claimer", "node", "cli", "prt"]
334336

335337
[auth.CARTESI_AUTH_PRIVATE_KEY]
336338
file = true
339+
file-policy = "strict-secret"
337340
go-type = "RedactedString"
338341
description = """
339342
The node will use this private key to sign transactions."""
@@ -342,6 +345,7 @@ used-by = ["claimer", "node", "cli", "prt"]
342345

343346
[auth.CARTESI_AUTH_MNEMONIC]
344347
file = true
348+
file-policy = "strict-secret"
345349
go-type = "RedactedString"
346350
description = """
347351
The node will use the private key generated from this mnemonic to sign transactions."""
@@ -382,6 +386,7 @@ used-by = ["claimer", "node", "cli", "prt"]
382386
[database.CARTESI_DATABASE_CONNECTION]
383387
default = ""
384388
file = true
389+
file-policy = "credential"
385390
go-type = "URL"
386391
description = """
387392
Postgres endpoint in the 'postgres://user:password@hostname:port/database' format (URL).

internal/config/generate/code.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ var funcMap = template.FuncMap{
5252
// For example, "int" becomes "toInt", "Duration" becomes "toDuration".
5353
return "to" + strings.ToUpper(goType[:1]) + goType[1:]
5454
},
55+
// toFilePolicyConst maps a Config.toml file-policy value to the
56+
// corresponding SecretFilePolicy constant name emitted in generated code.
57+
"toFilePolicyConst": func(policy string) string {
58+
switch policy {
59+
case filePolicyStrict:
60+
return "SecretFilePolicyStrict"
61+
case filePolicyCredential:
62+
return "SecretFilePolicyCredential"
63+
case "", filePolicyRegular:
64+
return "SecretFilePolicyRegularFileOnly"
65+
default:
66+
panic("invalid file-policy: " + policy)
67+
}
68+
},
5569
// splitLines splits a string into lines (by "\n").
5670
"splitLines": func(s string) []string {
5771
return strings.Split(s, "\n")
@@ -122,7 +136,6 @@ package config
122136
123137
import (
124138
"fmt"
125-
"os"
126139
"strings"
127140
128141
"github.com/spf13/viper"
@@ -221,7 +234,7 @@ func Get{{ toFieldName .Name }}() ({{ .GoType }}, error) {
221234
{{- if .File }}
222235
if s == "" {
223236
filename := viper.GetString({{toConstName .Name}}_FILE)
224-
contents, err := os.ReadFile(filename)
237+
contents, err := ReadConfigFileWithPolicy(filename, {{ toFilePolicyConst .FilePolicy }})
225238
if err != nil {
226239
return notDefined{{ .GoType }}(), fmt.Errorf("failed to parse %s: %w", {{ toConstName .Name }}_FILE, err)
227240
}

internal/config/generate/docs.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ func generateDocsFile(path string, env []Env) {
2525
"quote": func(s string) string {
2626
return `"` + s + `"`
2727
},
28+
"filePolicyDesc": func(p string) string {
29+
switch p {
30+
case filePolicyStrict:
31+
return "strict-secret (owner-only regular file; rejects group/world access)"
32+
case filePolicyCredential:
33+
return "credential (regular file; rejects world access)"
34+
case "", filePolicyRegular:
35+
return "regular-file (must be a regular file)"
36+
default:
37+
return p
38+
}
39+
},
2840
}
2941
tmpl := template.Must(template.New("docs").Funcs(funcMap).Parse(docsTemplate))
3042

@@ -56,6 +68,9 @@ This file documents the configuration options.
5668
{{.Description}}
5769
5870
* **Type:** {{backtick .GoType}}
71+
{{- if .File}}
72+
* **File policy:** {{filePolicyDesc .FilePolicy}}
73+
{{- end}}
5974
{{- if .Default}}
6075
* **Default:** {{.Default | quote | backtick}}
6176
{{- end}}

0 commit comments

Comments
 (0)