Skip to content

Commit 60c8803

Browse files
fix(pd): validate REST credentials and return 401 on refusal (#3189)
PD's REST interceptor decodes the Basic credential, keeps only the part before the colon, and checks it against the fixed set hg, store, hubble, vermeer. Any of those names with any password, including an empty one, is treated as an internal component; the password is never read. Separately, RestAuthentication.preHandle writes an error body without calling setStatus, so success, refusal and missing credential all return HTTP 200 and nothing keyed on a status code (monitors, curl -f, the shipped healthchecks) can see a refusal. The endpoints behind the interceptor mutate the cluster: POST /v1/members/change, DELETE /v1/store/{storeId}, graph and graphspace writes, and the balance and patrol tasks. The issue has the measured 27-request matrix. --------- Co-authored-by: imbajin <jin@apache.org>
1 parent e85b36a commit 60c8803

35 files changed

Lines changed: 1415 additions & 190 deletions

File tree

.github/workflows/pd-store-ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ jobs:
134134
done
135135
echo "can_run=true" >> "$GITHUB_OUTPUT"
136136
137+
- name: Run PD docker entrypoint secret override tests
138+
run: |
139+
$TRAVIS_DIR/test-pd-docker-entrypoint.sh
140+
141+
- name: Check every shipped PD config carries the REST hardening
142+
run: |
143+
$TRAVIS_DIR/test-pd-shipped-config.sh
144+
137145
- name: Run start-hugegraph-pd.sh foreground mode tests
138146
if: steps.pd-preflight.outputs.can_run == 'true'
139147
run: |

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ build/
4444
.env.test.local
4545
.env.production.local
4646
docker/.env
47+
# generated by docker/set-hubble-pd-password.sh, carries the PD REST secret
48+
docker/conf/hubble/*.local.properties*
4749

4850
*.orig
4951
*.rej

docker/README.md

Lines changed: 40 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,42 @@ contains a single quote or newline.
3939
echo ".env already exists; edit it instead of overwriting it" >&2
4040
exit 1
4141
}
42-
printf "HUGEGRAPH_ADMIN_PASSWORD='%s'\nHUGEGRAPH_AUTH_TOKEN_SECRET='%s'\n" \
43-
'replace-with-your-password' "${jwt_secret}" > .env
42+
pd_secret="$(openssl rand -hex 24)"
43+
printf "HUGEGRAPH_ADMIN_PASSWORD='%s'\nHUGEGRAPH_AUTH_TOKEN_SECRET='%s'\nHG_PD_AUTH_SECRET_KEY='%s'\n" \
44+
'replace-with-your-password' "${jwt_secret}" "${pd_secret}" > .env
45+
# Hubble reads the PD secret from a file, not from .env: generate the untracked properties files the HStore topologies mount.
46+
HG_PD_AUTH_SECRET_KEY="${pd_secret}" ./set-hubble-pd-password.sh hstore
47+
HG_PD_AUTH_SECRET_KEY="${pd_secret}" ./set-hubble-pd-password.sh hstore-ha
4448
)
4549
```
4650

47-
Do not commit `.env`. Keeping the same JWT secret preserves authentication
48-
tokens when containers are recreated. For authenticated topologies with
49-
multiple Server replicas, all replicas receive this same secret. The HA
50-
topology fails fast if authentication is enabled without this shared secret.
51+
Do not commit `.env` or `conf/hubble/*.local.properties`; both are in `.gitignore`. Keeping the same JWT secret preserves authentication tokens when containers are recreated. For authenticated topologies with multiple Server replicas, all replicas receive this same secret. The HA topology fails fast if authentication is enabled without this shared secret.
5152

52-
A non-empty `HUGEGRAPH_ADMIN_PASSWORD` enables Server authentication, and
53-
Hubble detects that mode automatically. Omitting the variable or setting it to
54-
an empty value disables authentication. Auth-off is only suitable for a
55-
trusted local environment; never expose it to a public or untrusted network.
56-
Hubble listens on host loopback by default. Set `HUBBLE_PUBLISH_HOST` only
57-
behind an HTTPS reverse proxy and trusted network controls.
53+
A non-empty `HUGEGRAPH_ADMIN_PASSWORD` enables Server authentication, and Hubble detects that mode automatically. Omitting the variable or setting it to an empty value disables authentication. Auth-off is only suitable for a trusted local environment; never expose it to a public or untrusted network. Hubble listens on host loopback by default. Set `HUBBLE_PUBLISH_HOST` only behind an HTTPS reverse proxy and trusted network controls.
5854

59-
`HUGEGRAPH_ADMIN_PASSWORD` initializes the built-in `admin` account on its
60-
first authenticated startup. Changing `.env` does not rotate an existing
61-
administrator password; use the HugeGraph user API for credential changes.
55+
`HUGEGRAPH_ADMIN_PASSWORD` initializes the built-in `admin` account on its first authenticated startup. Changing `.env` does not rotate an existing administrator password; use the HugeGraph user API for credential changes.
6256

63-
For the verification commands below, set the password in your current shell:
57+
For the verification commands below, load `.env` into your current shell and set the password:
6458

6559
```bash
60+
set -a; . ./.env; set +a
6661
ADMIN_PASSWORD='the-same-password-used-in-.env'
6762
```
6863

64+
The PD REST API (port 8620, HStore topologies only) requires HTTP Basic auth (`hg:${HG_PD_AUTH_SECRET_KEY}`) for all endpoints except health/readiness probes (`/v1/health`, `/v1/ready`). `HG_PD_AUTH_SECRET_KEY` is shared across PD, Server (`bin/wait-storage.sh`), and Hubble (`conf/hubble/*.local.properties` generated by `./set-hubble-pd-password.sh`).
65+
66+
Verify registered stores:
67+
68+
```bash
69+
curl -u "hg:${HG_PD_AUTH_SECRET_KEY}" http://localhost:8620/v1/stores
70+
```
71+
72+
To regenerate Hubble configuration after modifying `.env`:
73+
74+
```bash
75+
./set-hubble-pd-password.sh hstore # or hstore-ha
76+
```
77+
6978
### Standalone
7079

7180
This is the recommended quickstart.
@@ -181,8 +190,7 @@ Status:
181190
docker compose -f docker-compose-3pd-3store-3server.yml ps
182191
```
183192

184-
Verify all published PD, Store, and Server endpoints, Server authentication,
185-
and Hubble:
193+
Verify all published PD, Store, and Server endpoints, Server authentication, and Hubble:
186194

187195
```bash
188196
for port in 8620 8621 8622; do
@@ -202,34 +210,11 @@ done
202210
curl -fsS http://localhost:8088/about
203211
```
204212

205-
PD answers two unauthenticated probe endpoints. `/v1/health` is liveness only:
206-
it returns `200` as soon as the REST listener is up, even when the PD has no
207-
raft leader. `/v1/ready` returns `200` only while the PD sees a raft leader,
208-
and `503` otherwise. Each PD answers for itself: a single PD elects itself, and
209-
in a three-PD group the two that can reach each other elect a leader and turn
210-
ready, while a partitioned third keeps answering `503` until it sees that
211-
leader.
212-
213-
The healthchecks in these files still gate on `/v1/health`, because
214-
`/v1/ready` ships from the next release onwards while the files run published
215-
images. Two things to know before pointing them at readiness:
216-
217-
- Match on the body, not the status code. As of 1.7.0 PD answers `200` with
218-
`{"status":-1,"error":"Unauthorized!"}` on every path its auth interceptor
219-
does not exclude, a path that does not exist included, so a status-only
220-
probe reads a PD too old to have `/v1/ready` as ready. The body match holds
221-
whichever status a refusal carries. Gate with
222-
`curl -fsS http://localhost:8620/v1/ready | grep -q '"ready":true'` instead.
223-
- Pin `HUGEGRAPH_VERSION` to a release that carries the endpoint, or build the
224-
images from source with `docker-compose.dev.yml`.
225-
226-
The `HEALTHCHECK` baked into `hugegraph-pd/Dockerfile` is `/v1/health` as well.
227-
Both compose files override it, so it governs `docker run` and anything else
228-
inheriting the image probe, and those keep reading a PD without a quorum as
229-
healthy.
213+
PD answers two unauthenticated probe endpoints: `/v1/health` for liveness (returns 200 once the REST listener is up, regardless of raft state), and `/v1/ready` for readiness (returns 200 only when PD sees a raft leader, 503 otherwise).
230214

231-
Open `http://localhost:8088` and sign in as `admin` with the password from
232-
`.env`.
215+
Compose healthchecks currently gate on `/v1/health` for compatibility with published images. When targeting readiness on newer releases or source builds (`docker-compose.dev.yml`), match on the response body (`curl -fsS http://localhost:8620/v1/ready | grep -q '"ready":true'`).
216+
217+
Open `http://localhost:8088` and sign in as `admin` with the password from `.env`.
233218

234219
Stop containers while keeping them:
235220

@@ -265,9 +250,7 @@ HUBBLE_IMAGE=hugegraph/hubble:latest \
265250
docker compose -f docker-compose.yml up -d
266251
```
267252

268-
The Hubble `latest` image is expected to work with HugeGraph Server 1.7 and
269-
Server `latest`; compatibility with versions older than 1.7 is not promised.
270-
Pin immutable image references when reproducibility is required.
253+
The Hubble `latest` image is expected to work with HugeGraph Server 1.7 and Server `latest`; compatibility with versions older than 1.7 is not promised. Pin immutable image references when reproducibility is required.
271254

272255
### Server startup timeout
273256

@@ -348,16 +331,15 @@ docker compose -f docker-compose-hstore.yml up -d --wait
348331

349332
### Hubble configuration
350333

351-
The three small files under `conf/hubble/` contain only topology-specific
352-
discovery settings and container paths:
334+
The three small files under `conf/hubble/` contain only topology-specific discovery settings, the PD REST credential (`operations.pd.username` and `operations.pd.password`, which must match PD's `auth.secret-key`), and container paths:
353335

354336
- `conf/hubble/standalone.properties` uses direct Server mode.
355-
- `conf/hubble/hstore.properties` uses one PD and one Store REST target.
356-
- `conf/hubble/hstore-ha.properties` uses all three PD peers and all three
357-
allowed Store REST targets.
337+
- `conf/hubble/hstore.properties.example` uses one PD and one Store REST target.
338+
- `conf/hubble/hstore-ha.properties.example` uses all three PD peers and all three allowed Store REST targets.
339+
340+
The two HStore topologies mount the generated `*.local.properties` next to these examples (see `set-hubble-pd-password.sh`), never the examples themselves, so the PD secret stays out of tracked files.
358341

359-
Hubble detects Server authentication through the Server API. Do not add an
360-
`auth.enabled` property or duplicate auth-on/auth-off configurations.
342+
Hubble detects Server authentication through the Server API. Do not add an `auth.enabled` property or duplicate auth-on/auth-off configurations.
361343

362344
### Render and smoke checks
363345

@@ -367,12 +349,9 @@ Render every topology with auth-on inputs before submitting a change:
367349
bash test-compose.sh render
368350
```
369351

370-
The HA render is mandatory even when local resources are insufficient to start
371-
its ten containers.
352+
The HA render is mandatory even when local resources are insufficient to start its ten containers.
372353

373-
Run focused auth-on smoke checks for standalone and minimal HStore with the
374-
corresponding `up -d --wait`, status, authentication, Hubble `/about`, and
375-
`down -v` commands from the Users section:
354+
Run focused auth-on smoke checks for standalone and minimal HStore with the corresponding `up -d --wait`, status, authentication, Hubble `/about`, and `down -v` commands from the Users section:
376355

377356
```bash
378357
bash test-compose.sh smoke
@@ -384,6 +363,4 @@ Run the required local auth-off checks separately:
384363
bash test-compose.sh smoke-auth-off
385364
```
386365

387-
The auth-off mode is intentionally excluded from the default CI matrix and must
388-
remain on a trusted local machine. Both smoke modes remove only the isolated
389-
Compose projects and volumes that they create.
366+
The auth-off mode is intentionally excluded from the default CI matrix and must remain on a trusted local machine. Both smoke modes remove only the isolated Compose projects and volumes that they create.

docker/conf/hubble/hstore-ha.properties renamed to docker/conf/hubble/hstore-ha.properties.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ pd.enabled=true
2020
server.direct_url=http://server0:8080
2121
pd.peers=pd0:8686,pd1:8686,pd2:8686
2222
pd.server=pd0:8620
23+
# PD REST credential. The password must equal PD's auth.secret-key, which has
24+
# no default. Do not edit this tracked example: docker/set-hubble-pd-password.sh
25+
# generates the untracked .local.properties that Compose mounts, with the value
26+
# from HG_PD_AUTH_SECRET_KEY in .env.
27+
operations.pd.username=hubble
28+
operations.pd.password=
2329
operations.store.allowed_targets=[http://store0:8520,http://store1:8520,http://store2:8520]
2430
upload_file.location=/hubble/data/upload-files
2531
dashboard.address=

docker/conf/hubble/hstore.properties renamed to docker/conf/hubble/hstore.properties.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ pd.enabled=true
2020
server.direct_url=http://server:8080
2121
pd.peers=pd:8686
2222
pd.server=pd:8620
23+
# PD REST credential. The password must equal PD's auth.secret-key, which has
24+
# no default. Do not edit this tracked example: docker/set-hubble-pd-password.sh
25+
# generates the untracked .local.properties that Compose mounts, with the value
26+
# from HG_PD_AUTH_SECRET_KEY in .env.
27+
operations.pd.username=hubble
28+
operations.pd.password=
2329
operations.store.allowed_targets=[http://store:8520]
2430
upload_file.location=/hubble/data/upload-files
2531
dashboard.address=

docker/docker-compose-3pd-3store-3server.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ x-server-environment: &server-environment
7373
HG_SERVER_STARTUP_TIMEOUT_S: ${HG_SERVER_STARTUP_TIMEOUT_S-120}
7474
HG_SERVER_AUTH_TOKEN_SECRET: ${HUGEGRAPH_AUTH_TOKEN_SECRET:-}
7575
PASSWORD: ${HUGEGRAPH_ADMIN_PASSWORD:-}
76+
# bin/wait-storage.sh polls the PD REST API, so it needs the same secret
77+
PD_AUTH_PASSWORD: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
7678

7779
x-server-common: &server-common
7880
image: hugegraph/server:${HUGEGRAPH_VERSION:-latest}
@@ -110,6 +112,7 @@ services:
110112
HG_PD_INITIAL_STORE_LIST: store0:8500,store1:8500,store2:8500
111113
HG_PD_DATA_PATH: /hugegraph-pd/pd_data
112114
HG_PD_INITIAL_STORE_COUNT: 3
115+
HG_PD_AUTH_SECRET_KEY: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
113116
ports: ["8620:8620", "8686:8686"]
114117
volumes:
115118
- hg-pd0-data:/hugegraph-pd/pd_data
@@ -128,6 +131,7 @@ services:
128131
HG_PD_INITIAL_STORE_LIST: store0:8500,store1:8500,store2:8500
129132
HG_PD_DATA_PATH: /hugegraph-pd/pd_data
130133
HG_PD_INITIAL_STORE_COUNT: 3
134+
HG_PD_AUTH_SECRET_KEY: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
131135
ports: ["8621:8620", "8687:8686"]
132136
volumes:
133137
- hg-pd1-data:/hugegraph-pd/pd_data
@@ -146,6 +150,7 @@ services:
146150
HG_PD_INITIAL_STORE_LIST: store0:8500,store1:8500,store2:8500
147151
HG_PD_DATA_PATH: /hugegraph-pd/pd_data
148152
HG_PD_INITIAL_STORE_COUNT: 3
153+
HG_PD_AUTH_SECRET_KEY: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
149154
ports: ["8622:8620", "8688:8686"]
150155
volumes:
151156
- hg-pd2-data:/hugegraph-pd/pd_data
@@ -239,7 +244,15 @@ services:
239244
- "${HUBBLE_PUBLISH_HOST:-127.0.0.1}:8088:8088"
240245
volumes:
241246
- hubble-data:/hubble/data
242-
- ./conf/hubble/hstore-ha.properties:/hubble/conf/hugegraph-hubble.properties:ro
247+
- type: bind
248+
source: ./conf/hubble/hstore-ha.local.properties
249+
target: /hubble/conf/hugegraph-hubble.properties
250+
read_only: true
251+
bind:
252+
# The file is generated by set-hubble-pd-password.sh and is
253+
# gitignored. Without this, Docker would create an empty
254+
# directory at that path and Hubble would boot unconfigured.
255+
create_host_path: false
243256
healthcheck:
244257
test:
245258
- CMD-SHELL

docker/docker-compose-hstore.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ services:
3939
HG_PD_RAFT_PEERS_LIST: pd:8610
4040
HG_PD_INITIAL_STORE_LIST: store:8500
4141
HG_PD_DATA_PATH: /hugegraph-pd/pd_data
42+
HG_PD_AUTH_SECRET_KEY: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
4243
ports:
4344
- "8620:8620"
4445
volumes:
@@ -97,6 +98,8 @@ services:
9798
HG_SERVER_STARTUP_TIMEOUT_S: ${HG_SERVER_STARTUP_TIMEOUT_S-120}
9899
HG_SERVER_AUTH_TOKEN_SECRET: ${HUGEGRAPH_AUTH_TOKEN_SECRET:-}
99100
PASSWORD: ${HUGEGRAPH_ADMIN_PASSWORD:-}
101+
# bin/wait-storage.sh polls the PD REST API, so it needs the same secret
102+
PD_AUTH_PASSWORD: ${HG_PD_AUTH_SECRET_KEY:?set HG_PD_AUTH_SECRET_KEY in .env; see docker/README.md}
100103
ports:
101104
- "8080:8080"
102105
healthcheck:
@@ -120,7 +123,15 @@ services:
120123
- "${HUBBLE_PUBLISH_HOST:-127.0.0.1}:8088:8088"
121124
volumes:
122125
- hubble-data:/hubble/data
123-
- ./conf/hubble/hstore.properties:/hubble/conf/hugegraph-hubble.properties:ro
126+
- type: bind
127+
source: ./conf/hubble/hstore.local.properties
128+
target: /hubble/conf/hugegraph-hubble.properties
129+
read_only: true
130+
bind:
131+
# The file is generated by set-hubble-pd-password.sh and is
132+
# gitignored. Without this, Docker would create an empty
133+
# directory at that path and Hubble would boot unconfigured.
134+
create_host_path: false
124135
healthcheck:
125136
test:
126137
- CMD-SHELL

docker/set-hubble-pd-password.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one or more
4+
# contributor license agreements. See the NOTICE file distributed with
5+
# this work for additional information regarding copyright ownership.
6+
# The ASF licenses this file to You under the Apache License, Version 2.0
7+
# (the "License"); you may not use this file except in compliance with
8+
# the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# Generate the Hubble properties file a Compose topology mounts, with PD's
19+
# REST secret written in as operations.pd.password.
20+
#
21+
# usage: set-hubble-pd-password.sh <hstore|hstore-ha> [secret]
22+
#
23+
# Reads conf/hubble/<name>.properties.example (tracked) and writes
24+
# conf/hubble/<name>.local.properties (ignored by git), so the secret never
25+
# lands in a tracked file. The secret defaults to $HG_PD_AUTH_SECRET_KEY. The
26+
# value never goes through a sed replacement, where & # and backslash are
27+
# special, and backslashes are doubled for the .properties format. Run this
28+
# before `docker compose up`: the bind pins create_host_path: false, so a
29+
# missing target makes Compose refuse to start.
30+
#
31+
# The secret must be printable ASCII. PD compares it as UTF-8 bytes
32+
# (Authentication.verifySecret), while Hubble reads this file through
33+
# commons-configuration2, whose DEFAULT_ENCODING is ISO-8859-1, so a non-ASCII
34+
# secret decodes to different bytes on the two sides and gives a permanent 401
35+
# with no diagnostic anywhere. The README recipe generates hex, which is safe.
36+
set -euo pipefail
37+
38+
name=${1:?usage: $0 <hstore|hstore-ha> [secret]}
39+
secret=${2:-${HG_PD_AUTH_SECRET_KEY:-}}
40+
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/conf/hubble"
41+
example="${dir}/${name}.properties.example"
42+
out="${dir}/${name}.local.properties"
43+
44+
[[ -f "$example" ]] || { echo "no such topology: ${name} (expected ${example})" >&2; exit 1; }
45+
[[ -n "$secret" ]] || { echo "secret is empty; load .env first (set -a; . ./.env; set +a)" >&2; exit 1; }
46+
case "$secret" in
47+
*$'\n'*|*$'\r'*) echo "secret contains a line break, which a .properties value cannot hold" >&2; exit 1 ;;
48+
esac
49+
# LC_ALL=C so the range is ordinal and the walk byte-wise: under the caller's
50+
# collation a non-ASCII character can sort inside \x20-\x7e and slip through.
51+
is_printable_ascii() {
52+
local LC_ALL=C
53+
case "$1" in
54+
*[!$'\x20'-$'\x7e']*) return 1 ;;
55+
esac
56+
}
57+
is_printable_ascii "$secret" || {
58+
echo "secret must be printable ASCII: Hubble reads .properties as ISO-8859-1, PD compares as UTF-8" >&2
59+
exit 1
60+
}
61+
62+
escaped=${secret//\\/\\\\}
63+
# java.util.Properties skips whitespace between the separator and the value, so
64+
# a secret that starts with a space would reach Hubble shortened while PD and
65+
# the Server kept the original. A backslash before it keeps that first byte.
66+
case "$escaped" in
67+
[$' \t']*) escaped="\\${escaped}" ;;
68+
esac
69+
tmp=$(mktemp "${out}.XXXXXX")
70+
trap 'rm -f "$tmp"' EXIT
71+
{
72+
printf '# Generated from %s by set-hubble-pd-password.sh; not tracked by git.\n' "$(basename "$example")"
73+
grep -v '^operations\.pd\.password=' "$example" || true
74+
printf 'operations.pd.password=%s\n' "$escaped"
75+
} > "$tmp"
76+
# Hubble runs unprivileged and the mount is read-only, so the file must be world-readable
77+
chmod 644 "$tmp"
78+
mv "$tmp" "$out"
79+
trap - EXIT
80+
echo "wrote ${out}"

0 commit comments

Comments
 (0)