[Eclipse aeriOS] TLS certificate verification disabled by default for all outbound HTTPS (Keycloak, peer federators, shim)
> [!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/federator </details> <details> <summary><strong>What are the affected versions?</strong></summary> All versions. No git tags exist; `config.API_VERSION = "1.0.1"`; docker-compose references image `1.1.0`. Every revision in the repository carries the insecure default. </details> <details open> <summary><strong>Summary</strong></summary> `config.LoadEnvVars()` sets `TLS_CERTIFICATE_VALIDATION = false` when the environment variable is absent, and `main.go` then installs `&tls.Config{InsecureSkipVerify: true}` on `http.DefaultTransport`. Every outbound HTTPS call in the codebase — the Keycloak `client_credentials` POST that carries `CB_OAUTH_CLIENT_SECRET`, the peer-federator notifications that carry the Orion-LD bearer token, and the aerios-shim token fetch — flows through that transport, so a network-path adversary can MITM any of them and harvest the credentials. The shipped Helm `values.yaml` and `docker-compose.yaml` both explicitly set the variable to `false`, making the insecure mode the documented production default. </details> <details> <summary><strong>Severity</strong></summary> High </details> <details> <summary><strong>Weakness</strong></summary> CWE-295: Improper Certificate Validation </details> <details> <summary><strong>Location</strong></summary> - `config/config.go:127-136` — defaults `TLS_CERTIFICATE_VALIDATION` to `false` when unset - `main.go:20-22` — applies `InsecureSkipVerify: true` to `http.DefaultTransport` - `services/orionldAuthSvc.go:67-76` — `GetTokenFromKeycloak` POSTs `client_id`/`client_secret` via `http.DefaultClient` - `services/orionldAuthSvc.go:142-156` — `Interceptor.RoundTrip` attaches bearer token then delegates to `http.DefaultTransport` - `services/federatorSvc.go:43-49,95-101,142-148,171-178` — cross-domain calls use the `Interceptor` over `http.DefaultTransport` - `helm-chart/values.yaml:54` — `tlsCertificateValidation: false` - `docker-compose.yaml:20` — `TLS_CERTIFICATE_VALIDATION=false` - `test/.env:15` — `TLS_CERTIFICATE_VALIDATION=false` </details> <details> <summary><strong>Origin</strong></summary> Introduced in commit `d730017a4a0e2a31feba6258bd780efbce7e0d5d` (Rafa Vaño, 2025-11-07, "Initial code") — the very first code revision contains both the `false` default and the `InsecureSkipVerify` assignment. The Helm default `tlsCertificateValidation: false` was added in `9544fd5` ("Add Helm chart", 2025-11-07). </details> <details> <summary><strong>Methodology</strong></summary> Read `main.go` and `config/config.go` for transport setup, then traced every `http.Get`/`http.Post`/`http.DefaultClient.Do`/`client.Do` call site in `services/` to confirm they all resolve to `http.DefaultTransport` (either directly or via `Interceptor{core: http.DefaultTransport}`). Grepped for `InsecureSkipVerify`, `tls.Config`, `Transport` — the only TLS configuration in the binary is the global disable in `main.go`. Cross-checked all three deployment-config files for the shipped value. </details> <details> <summary><strong>Validation</strong></summary> Reproduction (`repro_tls_test.go`) starts an HTTPS server with a self-signed certificate, leaves `TLS_CERTIFICATE_VALIDATION` unset, replicates `main.go:20-22`, and invokes the real `GetTokenFromKeycloak`: ```go mitm := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) capturedBody = string(b) w.Write([]byte(`{"access_token":"stolen","expires_in":300}`)) })) os.Unsetenv("TLS_CERTIFICATE_VALIDATION") os.Setenv("APP_ENV", "production") os.Setenv("CB_TOKEN_MODE", "keycloak") os.Setenv("KEYCLOAK_URL", mitm.URL) os.Setenv("CB_OAUTH_CLIENT_SECRET", "super-secret-credential") config.LoadEnvVars() if !config.TLS_CERTIFICATE_VALIDATION { // replicates main.go:20-22 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } svc := &services.OrionLdAuthSvc{} tok, err := svc.GetTokenFromKeycloak() ``` Observed output (`go test -run TestTLSVerificationDisabledByDefault -v .`): ``` TLS_CERTIFICATE_VALIDATION env var not present, setting to false === config.TLS_CERTIFICATE_VALIDATION = false === Retrieving the token from Keycloak... Self-signed HTTPS server URL: https://127.0.0.1:38717 GetTokenFromKeycloak() error: <nil> Token returned by MITM server: "stolen" Body captured by MITM server: "client_id=ContextBroker&client_secret=super-secret-credential&grant_type=client_credentials" --- PASS ``` The connection to the self-signed server succeeds (no `x509: certificate signed by unknown authority` error), the OAuth client secret is captured in cleartext by the impostor, and the impostor's forged access token is accepted and would be cached in `config.OrionToken` for use as the bearer on subsequent broker calls. Mitigations checked and ruled out: no per-client `tls.Config` overrides anywhere in `services/`; no certificate pinning; no CA-bundle configuration option. The only knob is the boolean, and every shipped config sets it to the insecure value. </details> <details> <summary><strong>Detail</strong></summary> ```go // config/config.go:127-136 _, isTlsValPresent := os.LookupEnv("TLS_CERTIFICATE_VALIDATION") if !isTlsValPresent { log.Println("TLS_CERTIFICATE_VALIDATION env var not present, setting to false") TLS_CERTIFICATE_VALIDATION = false } // main.go:20-22 if !config.TLS_CERTIFICATE_VALIDATION { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } ``` Because the assignment mutates the process-global `http.DefaultTransport`, it affects: 1. `http.Get`/`http.Post` (used by `OrionldSvc` for the local broker — typically plain HTTP, but also `GetTokenFromShim` against `AERIOS_SHIM_URL`). 2. `http.DefaultClient.Do` (used by `GetTokenFromKeycloak`, `CheckTokenValidityInKeycloak`, `UpdateLocalDomainStatus`, `DeleteLocalDomainEntity`, `DeleteContextSourceRegistration`). 3. Every `&http.Client{Transport: &Interceptor{core: http.DefaultTransport, ...}}` — the inter-federator calls in `federatorSvc.go` and the federated broker queries in `orionldSvc.go`. `Interceptor.RoundTrip` adds `Authorization: Bearer <token>` and then calls `i.core.RoundTrip(req)`, i.e. the unverified transport. The Keycloak request body is `client_id=...&client_secret=...&grant_type=client_credentials` (`orionldAuthSvc.go:69`). The peer-federator and federated-broker requests carry `Authorization: Bearer <access_token>` (`orionldAuthSvc.go:152`). All cross the public internet in the documented multi-domain topology (`PEER_FEDERATOR_URL=https://other-domain.aerios-project.eu/federator`, `KEYCLOAK_URL=https://keycloak.aerios-project.eu` per `helm-chart/values.yaml`). </details> <details> <summary><strong>Prior art</strong></summary> `gh issue list`/`gh pr list` return nothing (repository has zero issues/PRs). `git log --all --grep` for `TLS|InsecureSkipVerify|certificate|CVE` returns nothing beyond the introducing commit. No existing report found. --- **Verification verdict:** TRUE POSITIVE ### Verification details Understood — let me complete the structured verification record. The analysis is done; this documents it in the required auditable format. (The negative/control PoC was already run — the secure-client rejection — I'll preserve its result here rather than re-run.) </details> <details> <summary><strong>Phase 1 — Data Flow Analysis</strong></summary> | Element | Finding | |---|---| | **Source (attacker input)** | The TLS server-side of any outbound HTTPS connection: a forged certificate + response body presented by an on-path adversary impersonating Keycloak / a peer federator / the shim. | | **Trust boundary crossed** | Federator process → public internet (production topology: `KEYCLOAK_URL=https://keycloak.aerios-project.eu`, `PEER_FEDERATOR_URL=https://other-domain.aerios-project.eu/federator`). The TLS layer *is* the boundary control, and it is disabled. | | **Sink** | `http.DefaultTransport` with `TLSClientConfig = &tls.Config{InsecureSkipVerify: true}` (`main.go:21`). All clients funnel here: `http.DefaultClient.Do` (`orionldAuthSvc.go:76,117`), `http.Get` (`orionldAuthSvc.go:34`), and `Interceptor{core: http.DefaultTransport}` (`federatorSvc.go:45,97,144,173`). | | **Data exposed at sink** | Outbound: `client_secret` (Keycloak POST body, `orionldAuthSvc.go:69`) and `Authorization: Bearer <token>` (`orionldAuthSvc.go:152`). Inbound: attacker-forged `access_token` accepted and cached (`orionldAuthSvc.go:100-101`). | | **API contract** | `http.DefaultTransport` is documented as a `*http.Transport`; the type assertion in `main.go:21` cannot fail. `crypto/tls` verifies against the system root store *unless* `InsecureSkipVerify` is set — which it is. | | **Environment protections** | None. No CA-bundle option, no cert pinning, no per-client `tls.Config`. `grep -r InsecureSkipVerify\|tls.Config\|TLSClientConfig` yields only the single global disable. The one control knob (`TLS_CERTIFICATE_VALIDATION`) defaults fail-open and is shipped `false` in Helm, compose, and test env files. | | **Cross-references** | `LoadEnvVars()` runs before the transport assignment in `main.go`; in `APP_ENV=production` (both shipped configs) `godotenv.Load` is skipped so the real env value governs. Confirmed reachable at process startup and on every token refresh (`GetTokenFromKeycloak` runs whenever `OrionToken.IsTokenExpired()`). | **Phase 1 result: PASS** — complete, unbroken flow from attacker-controlled TLS peer to credential exposure, with no intervening validation. </details> <details> <summary><strong>Phase 2 — Exploitability Verification</strong></summary> - **Attacker control — proven.** The executed PoC connected the *real* `GetTokenFromKeycloak` to a self-signed `httptest.NewTLSServer`; connection returned `err=<nil>` and the server received the cleartext secret. Attacker fully controls both the accepted certificate and the response body. - **Preconditions enumerated (complete adversarial analysis):** (1) `TLS_CERTIFICATE_VALIDATION` unset or `false` — satisfied by default and by all shipped configs; (2) target URL is `https://` — satisfied by production Helm config for Keycloak and peer; (3) on-path position between Federator and target — achievable via BGP hijack, DNS poisoning, or a malicious hop for an edge/IoT node. No credential or authenticated foothold is required. - **Mathematical bounds: N/A** — this is a configuration/crypto-verification flaw, not a memory/integer/bounds bug. No buffer or numeric range to bound. - **Race conditions: N/A** — trigger is deterministic and single-threaded (startup / synchronous token refresh); no TOCTOU or concurrency in the path. **Phase 2 result: PASS** — attacker control demonstrated empirically; preconditions are all satisfied by the shipped defaults. </details> <details> <summary><strong>Phase 3 — Impact Assessment</strong></summary> - **Real security impact (not mere robustness):** Disabling certificate verification defeats the *primary* confidentiality/authenticity control for these connections. Impact is concrete: (a) theft of the long-lived Keycloak `client_credentials` secret → persistent unauthorized access to the Context Broker realm; (b) theft of short-lived bearer tokens on every federation call; (c) acceptance of forged tokens the Federator then presents to real services. This is credential disclosure + authentication bypass, not an operational-resilience nicety. - **Primary vs defense-in-depth:** TLS verification here is the *only* line of defense on these links (no mTLS, no application-layer signing, no pinning). Its loss is not compensated elsewhere, so this is a primary-control failure, not a defense-in-depth gap. - **Severity:** High is warranted (CWE-295, credential disclosure over public internet). The sole limiting factor is the on-path-network precondition inherent to any MITM class. **Phase 3 result: PASS** — genuine, high-impact security consequence. </details> <details> <summary><strong>Phase 4 — PoC</strong></summary> **Pseudocode specification:** ``` 1. Start HTTPS server S with a self-signed cert (untrusted by system store). 2. Leave TLS_CERTIFICATE_VALIDATION unset; run config.LoadEnvVars(); point KEYCLOAK_URL at S. 3. Replicate main.go:20-22 (apply InsecureSkipVerify to DefaultTransport). 4. Call the real services.GetTokenFromKeycloak(). 5. Positive assertion: connection succeeds, S captures client_secret, forged token accepted. 6. Negative/control: a normal *http.Transport hitting S must FAIL with x509 error. ``` **Positive PoC — executed, result preserved:** ``` config.TLS_CERTIFICATE_VALIDATION = false err=<nil> token="stolen" body captured by impostor: "client_id=ContextBroker&client_secret=super-secret-credential&grant_type=client_credentials" --- PASS ``` **Negative PoC — executed, result preserved** (`TestControlSecureRejectsSelfSigned`, a standard `*http.Transport` against the same self-signed server): ``` secure client error: Get "https://127.0.0.1:43271": tls: failed to verify certificate: x509: certificate signed by unknown authority --- PASS ``` This is the negative case: with verification enabled the connection is refused, isolating `InsecureSkipVerify: true` as the necessary and sufficient cause. (Scratch test file was created under the repo, run, and removed per sandbox hygiene; both outputs are captured verbatim above.) **Phase 4 result: PASS** — pseudocode, executable positive PoC, and negative/control PoC all present with preserved output. </details> <details> <summary><strong>Phase 5 — Devil's Advocate (all 13)</strong></summary> 1. **Does the source actually reach the sink?** Yes — traced in Phase 1; PoC exercised the real function end-to-end. 2. **Is the input actually attacker-controlled?** Yes — the TLS peer cert and body are attacker-supplied in a MITM; empirically accepted. 3. **Is there upstream validation that neutralizes it?** No — grep confirms the only TLS config is the global disable; no CA/pinning path exists. 4. **Are there environment/config protections?** No — the one knob defaults fail-open and ships `false` everywhere. 5. **Is the code even reachable in normal execution?** Yes — runs at startup and on every token expiry; production configs use HTTPS targets. 6. **Is the bug class correctly identified?** Yes — CWE-295 Improper Certificate Validation; `InsecureSkipVerify: true` is the textbook instance. 7. **Could this be intended/by-design?** The knob exists deliberately, but the *insecure default* (fail-open when unset) and shipped `false` are the flaw; README describes it only as "skip certificate validation," with no warning — not a documented safe default. 8. **Is the impact overstated?** No — credential (client secret + bearer) disclosure and token forgery are demonstrable, not speculative. 9. **Is severity inflated by assumed network reach the component lacks?** Partially mitigated in classification: intra-cluster Keycloak (compose points CB at plain HTTP) is low-risk, but the *shipped production Helm* uses public HTTPS hostnames for Keycloak and peer, which is real network reach. Severity holds for the documented cross-domain topology. 10. **Does a simpler/benign explanation fit?** No — control PoC rules out any explanation other than disabled verification. 11. **Are there duplicate/compensating controls (mTLS, signing)?** No — none in the codebase. 12. **Would the PoC survive on the real deployment, not just a test rig?** Yes — the PoC calls the unmodified service function with the unmodified `LoadEnvVars` logic and the exact `main.go` transport assignment; the only substitution is the target URL. 13. **Is the finding already known / fixed / out of scope?** No prior issue/PR/CVE (repo has zero issues/PRs; git-log grep empty); present in initial commit and all shipped configs; component is in production scope. **Phase 5 result: PASS** — no challenge overturns the finding. </details> <details> <summary><strong>Gate Review</strong></summary> | Gate | Result | Evidence | |---|---|---| | **1. Process** | PASS | All five phases executed; both source-read and PoC evidence gathered, not pattern-matched. | | **2. Reachability** | PASS | Path reached at startup/token-refresh; PoC drove the real `GetTokenFromKeycloak` to the sink. | | **3. Real Impact** | PASS | Primary-control failure → client-secret + bearer-token disclosure and token forgery (Phase 3). | | **4. PoC Validation** | PASS | Positive PoC captured the secret; negative/control PoC failed with `x509` error, isolating the cause. | | **5. Math Bounds** | N/A (PASS) | Not a numeric/memory bug; no bounds to prove. | | **6. Environment** | PASS | No CA/pinning/mTLS mitigations; insecure value is the default and shipped in Helm/compose/test. | All applicable gates pass. </details> <details> <summary><strong>Final Verdict</strong></summary> **TRUE POSITIVE** — CWE-295. Certificate verification is unconditionally disabled on the process-global HTTP transport whenever `TLS_CERTIFICATE_VALIDATION` is unset or `false`, which is both the code default and the value in every shipped configuration; all outbound HTTPS (Keycloak client-secret POST, peer-federator and shim bearer-token calls) uses that transport. Empirically reproduced: an impostor server captured the client secret in cleartext and had its forged token accepted, while a verifying client rejected the same certificate. **EXPOSURE: REMOTE** — an unauthenticated on-path network adversary (BGP hijack, DNS poisoning of the Keycloak/peer hostname, or a malicious hop for an edge node) can MITM the connection with no credentials or prior foothold. **SCOPE: PRODUCTION** — shipped, deployable software (Helm chart, `docker-compose.yaml` with published image `eclipseaerios/federator:1.1.0`, `APP_ENV=production`); README: "a management service responsible for controlling the establishment and maintenance of federation mechanisms among the multiple aeriOS domains." No end-of-life/deprecation/superseded notice is present. </details> <details> <summary><strong>Steps to reproduce</strong></summary> An adversary with an on-path position between the Federator and either Keycloak or any peer domain (e.g. compromised upstream network, BGP hijack of the peer domain's prefix, DNS poisoning of `keycloak.*.aerios-project.eu`, or a malicious Wi-Fi/transit hop for an edge/IoT node — the project explicitly targets a "Cloud-Edge-IoT continuum") terminates TLS with any certificate and: - captures `CB_OAUTH_CLIENT_SECRET` on the next token refresh (triggered whenever `config.OrionToken` expires, including at startup), giving a permanent Keycloak `client_credentials` credential for the Context Broker realm; and/or - captures the short-lived bearer token attached to every `SpreadNewLocalDomain`/`NotifyNewDomain`/`NotifyDeletedDomain`/`CheckFederatorHealth`/`GetDomainEntities` call, giving immediate Context Broker access; and - can return a forged `access_token` that the Federator will then present to the real broker / peer federators. </details> <details> <summary><strong>Do you know any mitigations of the issue?</strong></summary> Change the default to secure: when `TLS_CERTIFICATE_VALIDATION` is unset, leave `http.DefaultTransport` untouched (Go verifies against the system trust store by default). Flip the shipped Helm/docker-compose values to `true`. If self-signed deployments must be supported, add a `CA_BUNDLE` option that appends to `RootCAs` instead of disabling verification globally. </details>
issue

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