[Eclipse aeriOS] Hardcoded Keycloak admin credentials exposed via NodePort in `idm` Helm chart
> [!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/resources
</details>
<details>
<summary><strong>What are the affected versions?</strong></summary>
`idm` chart 1.1.0 (the only version published in `charts/index.yaml`). The repository has no git tags; the issue is present from commit `a44028d` (2025-12-04) through HEAD (`62df22e`).
</details>
<details open>
<summary><strong>Summary</strong></summary>
The `idm` Helm chart — the Identity Manager for the entire aeriOS continuum — ships with the Keycloak master-realm administrator credential `admin` / `Pa55w0rd` baked into `values.yaml` and exposes the Keycloak HTTP service on a Kubernetes NodePort by default. The post-install NOTES.txt prints the access URL but does not warn the operator to change the password. An attacker who can reach any cluster node on the auto-assigned 30000–32767 port logs into the Keycloak admin console, creates a client/user with the `Continuum administrator` role, and mints JWTs that the aeriOS API Gateway (KrakenD) accepts for every protected route. The same chart also exposes the backing PostgreSQL on a NodePort with `keycloak` / `password`.
</details>
<details>
<summary><strong>Severity</strong></summary>
High
</details>
<details>
<summary><strong>Weakness</strong></summary>
CWE-1392: Use of Default Credentials
(also CWE-798: Use of Hard-coded Credentials, CWE-200: Exposure of Sensitive Information)
</details>
<details>
<summary><strong>Location</strong></summary>
- `charts/idm-1.1.0.tgz` → `idm/values.yaml` lines 55–64 (`keycloakPassword: Pa55w0rd`, `keycloakUser: admin`)
- `charts/idm-1.1.0.tgz` → `idm/values.yaml` lines 23, 91 (`service.type: NodePort` for both keycloak and database)
- `charts/idm-1.1.0.tgz` → `idm/values.yaml` lines 106–109 (`postgresPass: password`)
- `charts/idm-1.1.0.tgz` → `idm/templates/keycloak/deployment.yaml` lines 79–82 (env injection of the credential)
- `charts/idm-1.1.0.tgz` → `idm/templates/NOTES.txt` (no credential-change warning)
Related: `charts/openldap-stack-ha-4.1.2.tgz` → `values.yaml` lines 21–24 (`adminPassword: Not@SecurePassw0rd`) and `docker-compose/management-portal/docker-compose.yaml` line 34 (`LDAP_PASSWORD=Not@SecurePassw0rd`).
</details>
<details>
<summary><strong>Origin</strong></summary>
Commit `a44028d75b62938fe07c51904469a75f6bfc9551` (2025-12-04, author "Boret98", message "Added IdM and OpenLdap"). The chart was added with these defaults and has not been modified since.
</details>
<details>
<summary><strong>Methodology</strong></summary>
I extracted every `.tgz` under `charts/` and grepped the rendered trees for `password|secret|PASSWORD|adminPassword`, then for `type: NodePort|LoadBalancer` to find which credentialed services are externally exposed by default. The IdM chart was the only one combining a fixed administrator credential with default external exposure of the same service. I rendered the chart with `helm template` to confirm the credential reaches the Deployment manifest unaltered and that no Secret indirection, randomisation, or post-install warning intervenes.
</details>
<details>
<summary><strong>Validation</strong></summary>
```bash
#!/bin/bash
set -e
HELM=/tmp/linux-amd64/helm
CHART=charts/idm-1.1.0.tgz
echo "=== Rendering chart with default values (as per README quickstart) ==="
$HELM template idm "$CHART" > /tmp/idm-rendered.yaml
echo "=== KEYCLOAK_USER / KEYCLOAK_PASSWORD env vars in Deployment ==="
grep -A1 'KEYCLOAK_USER\|KEYCLOAK_PASSWORD' /tmp/idm-rendered.yaml
echo "=== Keycloak / database Service exposure ==="
python3 - <<'PY'
import yaml
for d in yaml.safe_load_all(open('/tmp/idm-rendered.yaml')):
if d and d.get('kind') == 'Service':
print(d['metadata']['name'], '->', d['spec']['type'], d['spec']['ports'])
PY
echo "=== NOTES.txt warning check ==="
$HELM install idm "$CHART" --dry-run 2>&1 | grep -i 'password\|warning\|change\|credential' \
|| echo "(no credential warning found in NOTES output)"
```
Observed output:
```
=== KEYCLOAK_USER / KEYCLOAK_PASSWORD env vars in Deployment ===
- name: KEYCLOAK_PASSWORD
value: "Pa55w0rd"
- name: KEYCLOAK_USER
value: "admin"
=== Keycloak / database Service exposure ===
idm-database-headless -> ClusterIP [{'port': 5432, 'targetPort': 5432, 'protocol': 'TCP'}]
idm-database -> NodePort [{'port': 5432, 'targetPort': 5432, 'protocol': 'TCP'}]
idm-keycloak -> NodePort [{'name': 'http', 'port': 8080, 'targetPort': 8080, 'protocol': 'TCP'}]
=== NOTES.txt warning check ===
(no credential warning found in NOTES output)
```
Mitigations checked and ruled out:
- The chart has no `existingSecret` path for the admin credential (only for TLS certs); the password is always taken from `.Values.keycloak.envVars.keycloakPassword`.
- No `randAlphaNum` or `derivePassword` Helm helpers are used.
- `KC_HOSTNAME_STRICT_HTTPS` is set to `false`, so the admin console is reachable over plain HTTP on the NodePort.
- The README (`helm install <name> eclipse-aerios/<chart>`) does not instruct users to override credentials.
</details>
<details>
<summary><strong>Detail</strong></summary>
`templates/keycloak/deployment.yaml` injects the values verbatim:
```yaml
- name: KEYCLOAK_PASSWORD
value: {{ .keycloakPassword | quote }}
- name: KEYCLOAK_USER
value: {{ .keycloakUser | quote }}
```
with `values.yaml` supplying:
```yaml
envVars:
keycloakPassword: Pa55w0rd
keycloakUser: admin
```
The image is `quay.io/keycloak/keycloak:legacy`, which honours `KEYCLOAK_USER`/`KEYCLOAK_PASSWORD` to bootstrap the master-realm admin on first start. `service.type: NodePort` causes kube-proxy to bind a port in 30000–32767 on every node's primary interface. `templates/database/service.yaml` does the same for PostgreSQL with `keycloak:password`.
The aeriOS API Gateway (`docker-compose/api-gateway/krakend.json` and the `api-gateway` Helm chart) validates JWTs against this Keycloak's JWKS endpoint and authorises by `realm_access.roles`. Master-realm admin can create realms, clients, and role mappings at will, so this credential is a bypass for every JWT-gated endpoint in the platform.
# Verification Report (complete): Hardcoded Keycloak admin credentials + default NodePort exposure in the `idm` Helm chart
</details>
<details>
<summary><strong>Verdict: TRUE POSITIVE — all six gates pass</strong></summary>
**Project status:** Active Eclipse **incubation** project (`eclipse_incubation` badge in README), most recent commit 2026-06-10. Not end-of-life, deprecated, archived, or superseded. Early-stage but maintained — a fix can be expected.
</details>
<details>
<summary><strong>Step 0 — Claim restated and routing</strong></summary>
**Restated claim:** A default `helm install` of `idm-1.1.0.tgz` renders a Keycloak Deployment whose master-realm admin is bootstrapped from a credential (`admin`/`Pa55w0rd`) that is hardcoded in the chart's public `values.yaml`, and renders a NodePort Service exposing that Keycloak on every node. Because the credential is published and the exposure is the default (and the documented access path), a network attacker logs in as master admin and pivots to full JWT/authz bypass across the platform; PostgreSQL is likewise NodePort-exposed with `keycloak`/`password`.
- **Root cause:** hardcoded working credential + default external exposure + no override forcing + no warning.
- **Bug class:** Use of hard-coded / default credentials + sensitive-service exposure (CWE-1392 / CWE-798 / CWE-200). Configuration/deployment finding, not memory-safety.
- **Threat model:** attacker with network reach to a cluster node; **no** prior credentials or foothold (the secret is public).
- **Routing decision:** The bug path crosses two artifacts (values → Deployment env → running Keycloak → Service → API Gateway trust), so it is mildly cross-component, but the class is well-understood, there is no concurrency/math, and the data flow is direct config-to-manifest. **Standard verification** is appropriate; I document each phase inline below.
</details>
<details>
<summary><strong>Phase 1 — Data-flow analysis</strong></summary>
| Element | Source location | Confirmed |
|---|---|---|
| Admin credential (source) | `idm/values.yaml:62-63` (`keycloakPassword: Pa55w0rd`, `keycloakUser: admin`) | ✓ |
| Injection (propagation) | `templates/keycloak/deployment.yaml:79-82` — `value: {{ .keycloakPassword \| quote }}` / `{{ .keycloakUser \| quote }}`, no Secret indirection | ✓ |
| Bootstrap (sink \#1) | image `quay.io/keycloak/keycloak:legacy` (WildFly distro) — entrypoint honors `KEYCLOAK_USER`/`KEYCLOAK_PASSWORD` to create the master-realm admin on first start | ✓ (documented upstream) |
| External exposure (sink \#2) | `values.yaml:23` `keycloak.service.type: NodePort`; passed through at `templates/keycloak/service.yaml:9`; auto-assigned 30000–32767 with `nodePort: ""` | ✓ |
| DB credential + exposure | `values.yaml:107-109` → `templates/database/statefulset.yaml:58-63`; `values.yaml:91` NodePort → `templates/database/service.yaml:9` | ✓ |
**Trust boundary:** the credential lives in a *public* GitHub repo (`eclipse-aerios/resources`) and the JWKS/roles it controls are trusted by the API Gateway (`realm_access.roles`). So the boundary between "public knowledge" and "master-realm admin authority" is crossed by design. **Environment protections:** none in-chart — no `existingSecret` for the admin credential (only for the TLS cert, `values.yaml:70`), no `randAlphaNum`/`derivePassword`, `hostnamestricthttps: false` keeps the console on plain HTTP, and `NOTES.txt` gives no warning. **Cross-reference:** the report's rendered output matches the templates line-for-line; the "Related" OpenLDAP/management-portal defaults (`Not@SecurePassw0rd`) were independently confirmed.
</details>
<details>
<summary><strong>Phase 2 — Exploitability verification</strong></summary>
- **Attacker control — CONFIRMED.** The attacker supplies the login request (`grant_type=password&client_id=admin-cli&username=admin&password=Pa55w0rd`) to the master realm token endpoint / admin console. Both the username and password are attacker-known because they are published in the chart; nothing the operator does by default changes them. The attacker fully controls the authenticating input and it reaches the authentication sink over the NodePort.
- **Mathematical bounds — N/A.** No arithmetic, buffer, or size condition is involved; there is nothing to bound.
- **Race conditions — N/A.** The exploit is a single synchronous authentication; no TOCTOU or concurrency in the trigger.
- **Adversarial analysis.** The only non-static precondition is *network reachability of a node's NodePort*. NodePort binds on every node's primary interface by kube-proxy; the chart's `NOTES.txt` itself advertises `http://$NODE_IP:$NODE_PORT` as the intended access URL, so reachability is the design posture, not an attacker assumption. Where node subnets are firewalled from the internet, the attacker is reduced to the node's network segment — still unauthenticated, still no foothold. No brute force is needed (credential is known), so there is no rate-limit or lockout defense to defeat.
</details>
<details>
<summary><strong>Phase 3 — Impact assessment</strong></summary>
**Real security impact, not operational robustness.** Success yields **master-realm administrator** of the platform IdM: the attacker can create realms/clients/users and assign roles (`Continuum administrator`, `ContextBroker`), then mint JWTs the API Gateway authorizes — a complete **authentication/authorization bypass** (privilege escalation + broad info disclosure) across every JWT-gated route. Independently, the PostgreSQL NodePort with `keycloak`/`password` gives direct DB read/write (info disclosure + integrity). This is a **primary control failure** (the identity system's admin authority), not a defense-in-depth degradation. Impact is High.
</details>
<details>
<summary><strong>Phase 4 — PoC</strong></summary>
**Pseudocode / operational PoC (attack chain):**
```
1. discover: nmap -p30000-32767 <node-ip> # find the auto-assigned Keycloak NodePort
2. auth: POST http://<node-ip>:<np>/auth/realms/master/protocol/openid-connect/token
grant_type=password client_id=admin-cli username=admin password=Pa55w0rd
-> master-realm access_token
3. pivot: use token to create user+role-mapping (Continuum administrator / ContextBroker),
obtain that user's JWT, present to API Gateway -> all protected routes accept it
4. bonus: psql -h <node-ip> -p <db-np> -U keycloak (password: password) -> direct DB R/W
```
**Executable PoC (deterministic static render).** A live cluster + `helm` are unavailable in this sandbox, so a full `helm template` run is **skipped with justification**: the relevant templates contain no randomness, no `lookup`, and no cluster-state dependence, so the rendered credential/exposure fields are a pure function of `values.yaml` — statically decidable. The script at `$TMPDIR/poc_verify.sh` asserts each source→manifest pairing and **all 8 positive assertions pass** (credential baked in, verbatim injection with no Secret indirection, both Services NodePort, DB credential baked in, plain-HTTP console, NOTES prints the node URL with no warning, no admin `existingSecret`). The credential-bootstrap behavior of the `:legacy` image is documented upstream and not re-proven live.
**Negative PoC (preconditions).** The same script encodes the two mitigations that each break the chain, proving neither is applied by default: (a) `--set keycloak.service.type=ClusterIP` removes the off-cluster binding; (b) `--set keycloak.envVars.keycloakPassword=<random>` invalidates the public credential. The chart provides **no forcing function** for either, and the README quickstart (`helm install ... eclipse-aerios/idm`) instructs neither — confirming the *default* install is the vulnerable one.
</details>
<details>
<summary><strong>Phase 5 — Devil's advocate (all 13 checklist items)</strong></summary>
1. **Full validation chain:** no upstream validation exists — the value flows straight from `values.yaml` to the container env. Traced; nothing intervenes.
2. **Conditional-logic reachability:** the credential env block is unconditional (`{{- with .Values.keycloak.envVars }}` is always non-empty by default); the NodePort branch renders because `type == NodePort`. No condition gates it off.
3. **Defensive-programming pattern?** No — this is not an assertion or a placeholder guarded by later checks; it is a live credential consumed at container start.
4. **Data-source trust:** source is a *public* chart value, i.e. attacker-knowable, not a trusted install-time-only internal secret.
5. **Bounds logic:** N/A (no arithmetic).
6. **TOCTOU:** N/A (single synchronous auth).
7. **API contract / trust boundary:** Keycloak's contract is exactly to authenticate whoever presents valid admin creds; presenting the published creds satisfies it — no built-in protection is bypassed or misread.
8. **Internal storage vs external input:** the credential is *not* a trusted-component-only internal value — it is externally reachable via NodePort and externally *known* via the public repo.
9. **Pattern vs analysis:** not mere pattern-matching on the word "password"; the finding rests on the *combination* of known credential + default external exposure + no override, each traced.
10. **Concurrent access possible?** N/A.
11. **Real vs theoretical impact:** concrete — master-admin of the IdM and DB R/W, not a cosmetic/robustness issue.
12. **Defense-in-depth vs primary control:** this *is* the primary control (identity system admin), so no compensating primary control exists behind it.
13. **Checklist applied rigorously:** every item above was evaluated, not skimmed.
Additional red-flag sweeps: not test/debug-only code (it is the published production chart, the only version in `index.yaml`); not unreachable; not framework-guaranteed-safe. No false-positive pattern applies.
</details>
<details>
<summary><strong>Gate review</strong></summary>
| Gate | Result | Basis |
|---|---|---|
| 1. Process | **PASS** | All phases completed with documented evidence (source lines + PoC). |
| 2. Reachability | **PASS** | Attacker fully controls the auth input; it reaches the sink over the default NodePort; static PoC confirms the path renders. Only network reachability is deployment-variable, and it is the documented posture. |
| 3. Real Impact | **PASS** | Master-realm admin → platform-wide authz bypass + DB R/W (privesc + info disclosure). |
| 4. PoC Validation | **PASS** | Pseudocode attack chain + deterministic static-render PoC (8/8 positive) + negative PoC (2 mitigations, neither default); live token exchange skipped with justification. |
| 5. Math Bounds | **PASS (N/A)** | No arithmetic condition; nothing mathematically prevents exploitation. |
| 6. Environment | **PASS** | No in-chart protection eliminates it — no Secret indirection for the admin cred, no randomization, plain HTTP, no warning; the two effective mitigations are non-default and unforced. |
All six gates pass → **TRUE POSITIVE**.
</details>
<details>
<summary><strong>Exposure and scope</strong></summary>
EXPOSURE: REMOTE — an unauthenticated network party who can reach any cluster node's auto-assigned NodePort (the chart's default and its `NOTES.txt`-documented access path) authenticates as Keycloak master admin with the publicly-known baked-in `admin`/`Pa55w0rd` and needs no prior account or foothold; only node-network firewalling is deployment-specific, not any secret the attacker must already hold.
SCOPE: PRODUCTION — the repository README states it "hosts **deployment and installation resources** for the Eclipse aeriOS project" and directs operators to `helm install ... eclipse-aerios/idm`; this is production deployment tooling (the identity manager for the whole continuum), not a demo, sample, or dev-only verification stack, and nothing marks it out of scope.
</details>
<details>
<summary><strong>Recommendation</strong></summary>
Real and worth fixing. Remove the literal default admin credential (fail the render when unset, or generate a random one into a Secret surfaced in `NOTES.txt`), default both Service types to `ClusterIP` with opt-in Ingress, and add a credential-change warning to `NOTES.txt`. Apply the same to the PostgreSQL NodePort/credentials and the related OpenLDAP / management-portal defaults.
</details>
<details>
<summary><strong>Steps to reproduce</strong></summary>
1. Operator follows the project README:
**Proof of concept:** [poc-1.txt](/uploads/b446a42348451c87a61a7de072806bc6/poc-1.txt)
2. Attacker finds the NodePort:
**Proof of concept:** [poc-2.txt](/uploads/bb1839727db84243cfb65235af3621e5/poc-2.txt)
(or, with any read access to the cluster, `kubectl get svc idm-keycloak`).
3. Attacker logs in:
**Proof of concept:** [poc-3.txt](/uploads/e179e680681f328a0e0a16ecd38780d3/poc-3.txt)
→ returns a master-realm `access_token`.
4. With the admin token the attacker creates a user in the application realm carrying the `Continuum administrator` and `ContextBroker` roles, obtains a JWT for that user, and presents it to the API Gateway. All 66 KrakenD-protected routes (Orion-LD context broker, federator, HLO orchestration, IOTA) accept it.
5. Independently, `psql -h <node-ip> -p <db-nodeport> -U keycloak` with password `password` yields direct read/write to the Keycloak database.
</details>
<details>
<summary><strong>Do you know any mitigations of the issue?</strong></summary>
Remove the literal default; either require the operator to set `keycloak.envVars.keycloakPassword` (fail the render if empty) or generate one with `randAlphaNum` stored in a Secret and surface it in NOTES.txt. Change both Service types to `ClusterIP` by default and let operators opt into external exposure via Ingress.
---
**Verification verdict:** TRUE POSITIVE
---
<!-- l1-helper-dup: f17b7407985d97ab6785697d661ceb27d9c53f2d501aca4a20c7bb3ebd8751fa -->
</details>
issue