[Eclipse aeriOS] Hardcoded, publicly-documented administrator credentials across the IdM stack
> [!warning] AI-Generated Vulnerability Report > This report was produced using AI-assisted security analysis, adversarially verified but not human-confirmed. This finding was challenged against the codebase and survived; we filter out low-confidence results rather than forwarding raw model output. Technical details, exploitability, and severity may still be wrong. Please validate independently before acting. <details open> <summary><strong>Basic information</strong></summary> **Project name:** Eclipse aeriOS **Project id:** iot.aerios **Repository:** https://github.com/eclipse-aerios/idm </details> <details> <summary><strong>What are the affected versions?</strong></summary> `idm` chart 1.1.0; `openldap-stack-ha` chart 4.1.2 as vendored here. All revisions since the repository was populated. </details> <details open> <summary><strong>Summary</strong></summary> The repository's documented deployment procedure stands up an internet-facing identity provider whose every administrative credential is fixed and published in the README. Following `README.md` verbatim yields a Keycloak master-realm admin of `admin:Pa55w0rd` on a NodePort service, an OpenLDAP directory admin of `cn=admin,dc=example,dc=org:Not@SecurePassw0rd`, and a set of seeded role-bearing application users (including `continuumadministrator1`) whose passwords are printed in two README files. The charts provide no `existingSecret` pathway for the Keycloak credentials, no random generation, no `required` guard, and no instruction to override — the README presents these as the credentials, not as placeholders. </details> <details> <summary><strong>Severity</strong></summary> High </details> <details> <summary><strong>Weakness</strong></summary> CWE-1392: Use of Default Credentials CWE-798: Use of Hard-coded Credentials </details> <details> <summary><strong>Location</strong></summary> Keycloak master-realm admin: - `helm/idm/values.yaml:62-63` — `keycloakUser: admin`, `keycloakPassword: Pa55w0rd` - `helm/idm/templates/keycloak/deployment.yaml:79-82` — injects as plain `env` (`KEYCLOAK_USER`/`KEYCLOAK_PASSWORD`), no Secret - `helm/idm/values.yaml:23` — `keycloak.service.type: NodePort` - `docker/docker-compose.yml:27-28,33` — `KEYCLOAK_USER: admin`, `KEYCLOAK_PASSWORD: Pa55w0rd`, published on `0.0.0.0:8080` OpenLDAP directory admin: - `helm/openldap/values.yaml:20-24` — `adminUser: "admin"`, `adminPassword: Not@SecurePassw0rd`, `configPassword: Not@SecurePassw0rd` - `README.md:78-79` and `helm/README.md:44-45` — publish these credentials verbatim Seeded privileged application users: - `helm/openldap/values.yaml:130-266` (`customLdifFiles.00-users.ldif`) — seeds `continuumadministrator1`, `verticaldeployer1`, `dataproductowner1`, `aeriosuser1`, `externaluser1` with weak/static `userpassword` and assigns them to roles `Continuum administrator`, `Vertical deployer`, etc. - `README.md:63-68` — publishes username=password credential pairs for these accounts - `helm/README.md:30-34` — publishes a different set of complex-looking passwords for the same accounts - `OpenLDAP_templates/Users/*.ldif` — additional user templates with `userpassword: {MD5}<username>` (effectively cleartext) Self-Service-Password token-encryption key: - `helm/openldap/charts/ltb-passwd/values.yaml:67-68` — `SECRETKEY: "password"` (the AES key LTB Self-Service-Password uses to encrypt password-reset tokens) </details> <details> <summary><strong>Origin</strong></summary> Commit `459fa98e95b1865d01b8d14d6cce5908ba2b18ba` ("Populated Repo", Boret98, 2025-12-04). Present since the first content commit. The OpenLDAP chart is forked from `jp-gouin/helm-openldap` v4.1.2; the upstream `Not@SecurePassw0rd` default is inherited, but the `customLdifFiles` payload with the aerOS role-bearing accounts is first-party to this repository. </details> <details> <summary><strong>Methodology</strong></summary> Read every `values.yaml`, every Deployment/StatefulSet template, and the three README files. Rendered the `idm` chart with `helm template` to confirm what reaches the cluster. Cross-referenced credential values against where the README instructs the operator to expose each service. Searched the `idm` templates for `secretKeyRef` and `randAlphaNum` (neither present for Keycloak admin or DB credentials) to confirm there is no secure override path. </details> <details> <summary><strong>Validation</strong></summary> Rendered chart shows the Keycloak admin password as a literal pod env var on a NodePort-exposed service: ```bash $ helm template idm ./helm/idm | grep -B1 -A1 -E 'KEYCLOAK_(USER|PASSWORD)|type: NodePort' spec: type: NodePort ports: -- - name: KEYCLOAK_PASSWORD value: "Pa55w0rd" - name: KEYCLOAK_USER value: "admin" ``` The README's quickstart (`README.md:97`) overrides only the NodePort number, nothing else: ``` helm install idm eclipse-aerios/idm --set keycloak.service.ports.keycloak.nodePort=<nodePort> ``` and immediately above (`README.md:90-91`) tells the operator that "Keycloak must be reachable by the user's browser" — i.e. the service is intended to be externally reachable. Confirming the OpenLDAP seed against the published credentials in `README.md:63-79`: ```bash $ grep -A2 'continuumadministrator1' helm/openldap/values.yaml uid: continuumadministrator1 uidnumber: 4001 userpassword: {CRYPT}continuumadministration1 $ grep -A2 'cn=Continuum administrator,ou=roles' helm/openldap/values.yaml dn: cn=Continuum administrator,ou=roles,dc=example,dc=org cn: Continuum administrator gidnumber: 601 memberuid: continuumadministrator1 $ grep -E 'adminPassword|configPassword' helm/openldap/values.yaml adminPassword: Not@SecurePassw0rd configPassword: Not@SecurePassw0rd ``` `helm/README.md:30-34` additionally publishes a *second* credential set (`continuumadministrator1 - VDw6!x7!8^O(`, `dataproductowner1 - 0oa9g44Nf/EM`, …). These do not match the LDIF in this repo but match the description "the last version … from the aeriOS common deployments repository", so they are likely the live credentials of the upstream packaged chart — also now public. Mitigations checked and absent: - No `randAlphaNum` / `derivePassword` in `helm/idm/templates/`. - No `existingSecret` value for Keycloak admin or DB; the only secret indirection in the idm chart is for the TLS cert. - No `{{ required "..." }}` guard on any credential. - No NOTES.txt warning to change passwords (`helm/idm/templates/NOTES.txt` is generic). - README files contain no instruction to override any credential before production use. </details> <details> <summary><strong>Detail</strong></summary> This repository is the identity layer for the aerOS continuum: every other aerOS component delegates authentication to this Keycloak, which in turn federates the OpenLDAP directory. The trust boundary the entire platform relies on is the set of admin credentials that protect (a) the Keycloak master realm, (b) the LDAP directory, and (c) the seeded role-bearing accounts that downstream services authorise against. All three are static and published. **Keycloak master admin (`admin:Pa55w0rd`).** `KEYCLOAK_USER`/`KEYCLOAK_PASSWORD` on the `legacy` (WildFly) image creates the initial admin in the `master` realm. Master-realm admin can create realms, create users in any realm, read every client secret, and rotate realm signing keys. The service is `NodePort` by default and the README directs operators to make it browser-reachable. There is no Secret indirection (`deployment.yaml:79-82` uses `value:` not `valueFrom:`), so an operator who *does* override the password still leaks it into the rendered Pod spec and Helm release history. **LDAP admin (`cn=admin,dc=example,dc=org:Not@SecurePassw0rd`).** Stored via `helm/openldap/templates/secret.yaml` but with the value taken from `values.yaml:21`. The README (`README.md:78-79`, `helm/README.md:44-45`) prints it verbatim as "the credentials" for phpLDAPadmin. The README's install command sets `--set phpldapadmin.service.type=NodePort`, exposing the LDAP admin UI on the node network with this published password. LDAP admin can create users in any role group, which Keycloak then federates as authorised users. **Seeded role accounts.** `customLdifFiles` is first-party content added to the vendored chart. It creates `continuumadministrator1` (member of role `Continuum administrator`), `verticaldeployer1`, etc. The README presents these as the platform's working accounts and publishes their passwords. Once federated through Keycloak, a token issued for `continuumadministrator1` carries the `Continuum administrator` role claim that downstream aerOS services use for authorisation. **LTB SECRETKEY.** The Self-Service-Password sub-chart uses `SECRETKEY` to AES-encrypt password-reset tokens sent to users. With the fixed value `password`, an attacker can forge a valid reset token for any LDAP user without access to the user's mailbox (combined with the ingress that is enabled by default at `ssl-ldap2.example`, `helm/openldap/values.yaml:625-634`). </details> <details> <summary><strong>Prior art</strong></summary> Searched `gh issue list` / `gh pr list` / `gh search issues` on `eclipse-aerios/idm` for `password`, `key`, `secret`, `credential` — no results. The repository has only two commits and no issues or PRs. The upstream `jp-gouin/helm-openldap` README documents `Not@SecurePassw0rd` as a default that operators should override; the aerOS-specific seeded accounts and the README publication of credentials are unique to this fork. --- **Verification verdict:** TRUE POSITIVE ### Verification details No helm binary in the sandbox, but the templates are trivial substitutions — `service.type: {{ .Values.keycloak.service.type }}` resolves to `NodePort`, `KEYCLOAK_PASSWORD` to the literal `Pa55w0rd` (`value:`, not `valueFrom:`), and the OpenLDAP secret to base64(`Not@SecurePassw0rd`) unless `existingSecret` is set (commented out by default). That confirms what reaches the cluster. Here is the completed structured verification. --- </details> <details> <summary><strong>Verification Report — Hardcoded/default admin credentials across the aerOS IdM stack</strong></summary> **VERDICT: TRUE POSITIVE** — CWE-798 (Hard-coded Credentials) / CWE-1392 (Default Credentials) **Bug class:** Credential management (hardcoded secrets). Memory-safety, integer, and concurrency sub-checks are N/A by class and are marked so below. ### Step 0 — Claim restated The chart/compose defaults ship fixed administrative credentials for every trust anchor in the identity stack, and the READMEs publish them verbatim as *the* credentials, with no secret indirection, no randomization, and no override guard — so a deployment that follows the docs is administered by publicly-known passwords. ### Phase 1 — Data Flow Analysis - **Sources / trust boundary:** The "source" is not attacker input to a parser; it is the credential *values* baked into the release. The trust boundary is the network edge of the deployed services (Keycloak master realm, OpenLDAP directory, phpLDAPadmin, LTB self-service-password). - **Flow to sink (Keycloak):** `helm/idm/values.yaml:62-63` → `helm/idm/templates/keycloak/deployment.yaml:79-82` renders `KEYCLOAK_PASSWORD` with `value: {{ .keycloakPassword | quote }}` (plaintext env, no `valueFrom`). The WildFly `legacy` image consumes `KEYCLOAK_USER`/`KEYCLOAK_PASSWORD` to create the master-realm admin. The service that fronts it: `helm/idm/templates/keycloak/service.yaml:9` → `type: {{ .Values.keycloak.service.type }}` = `NodePort` (default, `values.yaml:23`). - **Flow to sink (OpenLDAP):** `helm/openldap/values.yaml:20-24` → `helm/openldap/templates/secret.yaml` → `LDAP_ADMIN_PASSWORD: {{ .Values.global.adminPassword | b64enc }}`; emitted unless `global.existingSecret` is set, which is commented out (`values.yaml:18`), so the default path always uses `Not@SecurePassw0rd`. - **Flow to sink (seeded users / LTB key):** `customLdifFiles` (`helm/openldap/values.yaml:130-266`) is active by default and seeds role-bearing accounts into role groups; `helm/openldap/charts/ltb-passwd/values.yaml:67-68` sets `SECRETKEY: "password"`. - **API contracts:** Helm value contract offers `existingSecret` **only** for the Keycloak TLS cert and for the OpenLDAP admin secret — **not** for the Keycloak admin/DB credentials. No contract exists to inject those from an external secret. - **Environment protections:** None in-band. No `{{ required }}`, no `randAlphaNum`/`derivePassword`, no NOTES.txt warning (`helm/idm/templates/NOTES.txt` is generic). Out-of-band protection (cluster firewall) is deployment-dependent and the docs steer against it (see Phase 3). - **Cross-references:** `README.md:63-79`, `helm/README.md:30-45` publish the credentials; `docker/docker-compose.yml:27-28,32-33` hardcodes `admin:Pa55w0rd` and binds `8080:8080` to the host. Two different published password sets exist for the seeded accounts (README vs helm/README), both public. ### Phase 2 — Exploitability Verification - **Attacker control:** CONFIRMED. The attacker does not need to *control* input — the credentials are public. Attacker need only reach the port and authenticate. Reachability confirmed via `NodePort` default and docker-compose host binding. - **Mathematical bounds:** N/A (no buffer/length/index arithmetic in this bug class). - **Race conditions / TOCTOU:** N/A (no concurrent state transition in the trigger; login is idempotent). - **Adversarial analysis:** The only plausible defeater is network isolation. The documented deployment defeats isolation itself: `README.md:88-91` instructs that "Keycloak must be reachable by the user's browser," and the OpenLDAP install command sets `phpldapadmin.service.type=NodePort`. So under the documented path the precondition holds. Established as a live, reachable weakness. - **Nuance found:** the seeded LDAP passwords are stored as e.g. `userpassword: {CRYPT}continuumadministration1` — `continuumadministration1` is not a valid crypt(3) hash, so those *specific* seeded accounts may not authenticate with the literal README string. This weakens only the "seeded accounts" sub-claim; the Keycloak master admin and OpenLDAP directory admin (the decisive anchors) are stored/consumed as plaintext and are fully functional. ### Phase 3 — Impact Assessment - **Real security impact (primary control):** Yes. These credentials *are* the primary authentication control for the platform's identity provider. Compromise = master-realm admin (create realms/users, read every client secret via `GET /admin/realms/{r}/clients/{id}/client-secret`, rotate signing keys) and directory admin (inject users into any role group that downstream aerOS services authorize against). This is authentication bypass of the whole platform's trust root, not a hardening nit. - **Defense-in-depth vs primary:** This is the primary control failing, not a secondary layer. Even an operator who overrides the password still leaks it into the rendered pod spec and Helm release history because `deployment.yaml` uses `value:` rather than a secret ref — a genuine secondary weakness worth noting but not the crux. ### Phase 4 — Proof of Concept Network egress is sandboxed, so a live run is not possible here; the PoC is documented and the rendered-manifest facts were verified directly from the templates above. - **Pseudocode PoC:** 1. Operator runs the README quickstart (`helm install idm … --set keycloak.service.ports.keycloak.nodePort=<p>`), or `docker-compose up -d`. 2. Attacker reaches Keycloak on the NodePort / host `:8080`. 3. Attacker POSTs the published credentials to the master-realm token endpoint → receives an admin access token → full admin API. - **Concrete reproduction (from any host that can reach the port):** ```bash TOKEN=$(curl -s "http://<node-ip>:<port>/auth/realms/master/protocol/openid-connect/token" \ -d 'grant_type=password&client_id=admin-cli&username=admin&password=Pa55w0rd' | jq -r .access_token) curl -s -H "Authorization: Bearer $TOKEN" "http://<node-ip>:<port>/auth/admin/realms" | jq '.[].realm' ``` Equivalent for LDAP: log into phpLDAPadmin (NodePort) as `cn=admin,dc=example,dc=org` / `Not@SecurePassw0rd`. - **Rendered-manifest evidence (validated here):** `deployment.yaml` emits `KEYCLOAK_PASSWORD` as literal `"Pa55w0rd"` (plaintext env), `service.yaml` emits `type: NodePort`, and `secret.yaml` emits `LDAP_ADMIN_PASSWORD = base64("Not@SecurePassw0rd")` by default. These follow deterministically from the template substitutions shown above. - **Negative PoC (preconditions that make it fail):** (a) operator overrides every credential AND fronts the service so the plaintext never reaches an exposed surface; (b) `global.existingSecret` is set for OpenLDAP; (c) the service is firewalled to a trusted network the attacker cannot reach. None of these are the default or the documented path, which is exactly why the finding is real for a doc-following operator. The seeded-user login also fails as a sub-case where the `{CRYPT}` value is not a valid hash (noted in Phase 2). ### Phase 5 — Devil's Advocate (13 challenges) 1. **Restates coherently?** Yes — "shipped defaults are public admin passwords with no override path." 2. **Placeholder vs real default?** Real default: published as "the credentials," no guard, install commands don't override. 3. **Upstream validation/neutralization?** None; the value flows straight to the container env / LDAP secret. 4. **Guarded by config that's off by default?** No — NodePort and the plaintext env are the defaults; `customLdifFiles` is uncommented. 5. **Reachable in normal execution?** Yes — this is the normal, documented deployment path. 6. **Caller constraints prevent it?** N/A — no caller gate; login is the entry point. 7. **Already-known/accepted risk?** Partly: the OpenLDAP `Not@SecurePassw0rd` is a documented upstream default meant to be overridden — but this fork *publishes* it and adds first-party seeded accounts + README credential publication, which is new. 8. **Severity inflated?** Slightly for the seeded-account sub-claim (invalid `{CRYPT}` hash); Keycloak/LDAP admin severity is accurate. High (not Critical) is the right call given the network-reach precondition. 9. **Right root cause?** Yes — hardcoded defaults + no secret indirection + doc publication. 10. **Test/sample/dead code?** No — production deployment charts and compose file; the seeds ship by default. 11. **Needs unrealistic preconditions?** No — the only precondition (network reach) is what the README instructs. 12. **Better explanation than "vuln"?** No benign explanation; these are live credentials on an exposed service. 13. **Environment/sandbox neutralizes it?** Only a firewall an operator must add themselves; the docs steer toward exposure, not isolation. ### Gate Review - **Gate 1 — Process:** PASS. All phases executed; math/race marked N/A by class. - **Gate 2 — Reachability:** PASS. Default `NodePort` + docker-compose host bind + README exposure instruction put the auth surface on the network. - **Gate 3 — Real Impact:** PASS. Primary authentication control of the platform's identity root is compromised. - **Gate 4 — PoC Validation:** PASS (documented + manifest facts validated from templates; live curl blocked by sandbox egress, not by any missing precondition). - **Gate 5 — Math Bounds:** N/A (not a numeric/memory bug). - **Gate 6 — Environment:** PASS. No in-band protection; the sole mitigation is operator-added network isolation the docs discourage. All applicable gates pass → **TRUE POSITIVE**. --- EXPOSURE: REMOTE — an unauthenticated network attacker who reaches the default-`NodePort` Keycloak service (which `README.md:88-91` directs operators to make browser-reachable, and which `docker-compose.yml` binds to the host on `:8080`) authenticates as `admin:Pa55w0rd` and gains master-realm admin over the platform's identity provider. SCOPE: PRODUCTION — these are the real deployment charts and compose file for the aerOS platform's identity layer, presented for live pilot deployments ("It will depend on the specific needs or network configuration of the pilot"), not documented as a dev-only, demo, or out-of-scope component. </details> <details> <summary><strong>Steps to reproduce</strong></summary> Following the README quickstart on a fresh cluster, then from any host that can reach the Keycloak NodePort: **Proof of concept:** [poc.sh](/uploads/3d443e483d1525789b420f768309d23a/poc.sh) This yields a master-realm admin token. From there the attacker can enumerate every realm, dump every client secret (`GET /admin/realms/{r}/clients/{id}/client-secret`), and create a user with any role in any realm. Equivalently, log into phpLDAPadmin on its NodePort with the published `Not@SecurePassw0rd` and add a user to the `Continuum administrator` role group. </details> <details> <summary><strong>Do you know any mitigations of the issue?</strong></summary> For the `idm` chart: remove the literal defaults for `keycloakPassword`, `dbPassword`, `postgresPass`; add `existingSecret` support and source credentials via `valueFrom.secretKeyRef`; fall back to `randAlphaNum 32` if neither override nor existing secret is provided; emit the generated value in `NOTES.txt`. For OpenLDAP: drop the seeded role-bearing accounts from `customLdifFiles` (ship them as documentation, not defaults) and remove all credential values from both READMEs. For ltb-passwd: generate `SECRETKEY` per-release. Add a "before production" section to the README that lists every credential that must be set. <!-- l1-helper-dup: b7c67b44f0704478c847eaa7ffd300847219372ac3ebc9e6bffad7da7db3a131 --> </details>
issue

Copyright © Eclipse Foundation AISBL. All rights reserved.     Privacy Policy | Terms of Use | Copyright Agent