Skip to content

fix(pd): validate REST credentials and return 401 on refusal - #3189

Open
bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/pd-rest-auth-3188
Open

fix(pd): validate REST credentials and return 401 on refusal#3189
bitflicker64 wants to merge 3 commits into
apache:masterfrom
bitflicker64:fix/pd-rest-auth-3188

Conversation

@bitflicker64

@bitflicker64 bitflicker64 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Purpose of the PR

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.

PD REST auth before and after: name-only check where every outcome returns 200, versus name plus shared secret where refusals return 401 and probes stay open

Main Changes

  • Authentication.authenticate now reads the password and compares it (constant-time, MessageDigest.isEqual) with the shared secret from auth.secret-key, a PDConfig key that already existed but was only referenced by dead code. The service-name check stays. A missing or empty secret refuses every request instead of falling back to name-only authentication.
  • RestAuthentication.preHandle sets 401 for a missing, malformed or refused credential. The JSON error body is unchanged.
  • auth.secret-key now appears in both shipped application.yml files with a change-in-production note, and the PD Docker image accepts an optional HG_PD_AUTH_SECRET_KEY env, written into SPRING_APPLICATION_JSON and never logged.
  • In-repo clients are wired to match: wait-storage.sh defaults its PD password to the shipped secret (its old admin default only passed because the password was ignored), and the Compose Hubble properties files set operations.pd.username and operations.pd.password.
  • PD test credentials switch from store:123 / store:admin to the shipped secret, and RestApiTest gains four cases asserting 401 for a missing credential, a wrong password, an empty password and an unknown service name.
  • The hugegraph-pd and docker READMEs document the credential and state that the shipped default secret is public, so operators must change it or keep port 8620 off shared networks.

This intentionally refuses any client that sent a valid name with an arbitrary password, which is why it targets 1.8.0 (see the issue for the release-boundary reasoning). Remaining wiring lives outside this repo: Hubble's own PD calls on the toolchain side, the Helm chart's Secret in #3132, and the same note for the website docs. Probes are untouched; #3185 keeps /v1/ready off the authenticated surface for exactly that reason.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • RestApiTest (run by pd-rest-test in CI against a live PD) now asserts 401 for a missing credential, a wrong password, an empty password and an unknown service name, and keeps asserting 200 with a valid credential on the existing endpoints.
    • Measured on 2026-09-03 against the packaged dist (JDK 11): /v1/members and /v1/stores return 401 for no header, hg: with an empty password, hg:wrongpassword and nobody:<secret>, and 200 only for hg:<secret> and store:<secret>; /v1/health stays 200 without a credential.
    • PDRestSuiteTest 17/17, PDClientSuiteTest 45/45, test-wait-storage.sh 5/5, docker/test-compose.sh render and mvn editorconfig:check on the touched PD modules all pass locally.

Does this PR potentially affect the following parts?

  • Dependencies (add/update license info & regenerate_known_dependencies.sh)
  • Modify configurations
  • Other affects (REST clients that authenticated with a valid service name and an arbitrary password are refused starting with this change; every credential in this repo is updated in the same commit)
  • Nope

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

The REST authentication interceptor had two defects (apache#3188):

1. Authentication.authenticate decoded the Basic credential but checked
   only the service name against the innerModules set, so any of the four
   public names with any password, including an empty one, was accepted,
   while the password was never read.

2. RestAuthentication.preHandle wrote an error body without calling
   setStatus, so success, refusal and missing credential all returned
   HTTP 200 and nothing keyed on a status code could see a refusal.

Fix both together:

- Compare the password of the Basic credential against the shared secret
  configured via auth.secret-key (constant-time comparison). A missing or
  empty secret refuses every request instead of falling back to name-only
  authentication.
- Return 401 for a missing, malformed or refused credential.
- Surface auth.secret-key in both shipped application.yml files with a
  change-in-production note, and let the Docker image set it through a
  new optional HG_PD_AUTH_SECRET_KEY env (never logged).
- Wire the in-repo clients: wait-storage.sh now defaults its PD password
  to the shipped secret, and the Compose Hubble properties files carry a
  matching operations.pd.username/password pair.
- Update the PD test credentials to the shipped secret and add REST tests
  asserting 401 for missing credential, wrong password, empty password
  and unknown service name.
- Document the credential and the trusted-network requirement for port
  8620 in the PD and Compose READMEs.

Probes are unaffected: /v1/health, /actuator/* and /v1/prom/targets/*
stay outside the interceptor.
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.98%. Comparing base (98477f0) to head (5c339c1).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...gegraph/pd/service/interceptor/Authentication.java 0.00% 14 Missing ⚠️
.../java/org/apache/hugegraph/pd/config/PDConfig.java 0.00% 3 Missing ⚠️
...egraph/pd/rest/interceptor/RestAuthentication.java 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3189      +/-   ##
============================================
- Coverage     37.78%   36.98%   -0.81%     
+ Complexity     6556     6355     -201     
============================================
  Files           800      785      -15     
  Lines         68929    66873    -2056     
  Branches       9157     8898     -259     
============================================
- Hits          26046    24731    -1315     
+ Misses        39824    39202     -622     
+ Partials       3059     2940     -119     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocking: yes. Summary: The gate itself is right (name check, constant-time comparison, fail-closed on an empty secret, and the long-missing setStatus), but one blocker, two important items and four nits below. The blocker: PDConfig.java:72 defaults auth.secret-key to a quoted literal, so a PD whose config file omits the key rejects the secret this PR documents, and wait-storage.sh then burns its 300s timeout and aborts Server startup. The two important ones are the same failure reached by rotating the secret per docker/README.md, and the actuator exposure list on the port being hardened. Evidence: new StandardEnvironment().resolvePlaceholders("${auth.secret-key: 'FXQ...'}") on spring-core 5.3.20, the line spring-boot-starter-web:2.5.14 resolves, returns " 'FXQXbJtbCLxODc6tGci732pkH1cyf8Qg'", leading space and quotes included. CI at f796637 is green on pd, store, struct, CodeQL and every build-server job; the red hstore job is VertexCoreTest.testQueryByJointIndexesWithSearchAndTwoRangeIndexesAndWithin (expected 3, was 1), the index-ordering defect #3182 addresses, not this change.

Comment thread hugegraph-pd/README.md
Comment thread docker/README.md Outdated
Comment thread hugegraph-server/hugegraph-dist/src/assembly/static/bin/wait-storage.sh Outdated
Comment thread hugegraph-pd/README.md Outdated

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: no. Summary: The new REST credential remains a fixed repository-published secret while the default HStore Compose topologies publish PD's management port to the host. This makes the credential reusable by anyone who can reach those ports, so deployment protection still depends on operators noticing the warning. Evidence: exact head f796637; the added shipped configuration contains a fixed auth.secret-key, while docker-compose-hstore.yml and docker-compose-3pd-3store-3server.yml publish PD REST ports. Please require a deployment-provided or generated secret, or fail startup while the public default remains.

Comment thread hugegraph-pd/hg-pd-dist/src/assembly/static/conf/application.yml Outdated
Review follow-ups on the REST credential change.

The blocker: PDConfig declared the key as
`@Value("${auth.secret-key: 'FXQ...'}")`. Spring takes the text after the
first colon as a literal default, so a PD whose conf/application.yml has no
auth block resolved the secret to " 'FXQ...'", quotes and leading space
included. That is neither empty nor anything a client sends, so the
fail-closed branch never ran and PD rejected the secret shipped in both
config files, wait-storage.sh and the Hubble properties. Since
start-hugegraph-pd.sh passes -Dspring.config.location, which replaces the
default locations rather than adding to them, an upgrade that keeps an
existing config file hit this and Server startup aborted after the 300s
wait-storage timeout. Drop the default so an absent key yields "", and log
an error naming the parameter once so the refusal is diagnosable.

Also from the review:

- Pass the secret to every consumer from one variable. Both Compose files
  now set HG_PD_AUTH_SECRET_KEY on each PD service and PD_AUTH_PASSWORD on
  each Server, so rotating one .env value keeps the Server's wait-storage.sh
  probe working. Hubble still needs a manual edit of its mounted properties
  file, which the docs now say.
- Narrow the actuator exposure from "*" to health,metrics,prometheus in both
  configs. /actuator/* is excluded from the interceptor, so anything exposed
  there is anonymous on the port this change is hardening.
- Send WWW-Authenticate on the 401, per RFC 7235. Without it clients that
  authenticate reactively never retry with credentials.
- Record in the GRpcServerConfig TODO that the secret check now lives in the
  shared base class, so enabling the gRPC interceptor also requires giving
  the Server, Store and CLI clients the secret.
- Document the credential in the PD configuration and API references, and
  credential the balanceLeaders rebalancing procedure in the Store
  operations guide, where a 401 reads as a no-op during an incident.

Verified against the packaged dist: with the auth block removed from
conf/application.yml every authenticated request is refused and the error
names auth.secret-key; with it present the matrix is unchanged, the 401
carries the challenge header, and /actuator/env, /beans and /configprops no
longer serve data.

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: no. Summary: REST credential validation and status handling are wired correctly. One edge case remains in the Docker secret override: raw JSON control characters can make the generated Spring configuration invalid. Evidence: an exact-head shell harness with a carriage return in HG_PD_AUTH_SECRET_KEY makes SPRING_APPLICATION_JSON invalid; Codecov failures are non-blocking.

# the value from conf/application.yml applies. Never logged.
AUTH_JSON=""
if [[ -n "${HG_PD_AUTH_SECRET_KEY:-}" ]]; then
AUTH_JSON="\"auth\": { \"secret-key\": \"$(json_escape "${HG_PD_AUTH_SECRET_KEY}")\" },"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

⚠️ The optional secret is inserted through json_escape, but that helper only escapes backslashes and quotes and removes LF; it leaves other JSON control characters such as CR and tab unescaped. Reproduction: on this exact head, setting HG_PD_AUTH_SECRET_KEY to a value containing a\rb produces an invalid SPRING_APPLICATION_JSON, so the PD container fails before startup. Please escape all JSON control characters or avoid hand-building this JSON, and add a test for the override.

Addresses the two remaining review points.

A published default is not a secret. The REST credential shipped with a
fixed value that lives in this repository, on a port the HStore Compose
files publish to the host, so anyone who could read the source could
authenticate as an internal service against endpoints that rewrite the raft
peer list, remove stores and move data. Requiring a deployment-provided
secret is the only version of this check that means anything.

- Both application.yml files now ship auth.secret-key empty, with a comment
  saying why there is no default and how to generate one. PD already refuses
  every authenticated REST request while it is empty, naming the key in an
  error.
- PD refuses to start when auth.secret-key is set to the value earlier
  revisions carried as a placeholder, so a deployment that copied it does not
  quietly keep a well-known credential.
- The Docker image requires HG_PD_AUTH_SECRET_KEY, and both Compose files
  fail fast when it is unset rather than falling back to a shared value. The
  .env recipe generates one alongside the JWT secret. The Hubble properties
  files ship the password empty, with the manual step documented.
- travis/start-pd.sh supplies a test-only secret through
  SPRING_APPLICATION_JSON, matching what the PD suites send, since the
  shipped configuration no longer authenticates anything.

wait-storage.sh no longer interpolates the credential into the inner
bash -c string, where a secret containing a space, a backtick or $(...)
would have split the arguments or run, and no longer passes it in argv where
anything able to read /proc could see it. The inner shell reads the value
from the environment and hands it to curl on stdin as a config file. Its
test now asserts the credential is absent from argv and present in that
config.

Verified against the packaged dist: startup is refused with the published
placeholder, the shipped configuration starts but answers 401 to every
credential, and a deployment-provided secret restores the matrix with the
challenge header. PDRestSuiteTest 17/17, PDClientSuiteTest 45/45,
test-wait-storage.sh 5/5, compose renders pass and refuse to render without
the secret.
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.

[Bug] PD REST auth checks only the service name, never the password, and returns HTTP 200 on refusal; fix both in 1.8.0

2 participants