[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