Summary
The localLoginHandlers struct in the Juju API server maintains an in-memory map to store discharge tokens following successful local authentication. This map is accessed concurrently from multiple HTTP handler goroutines without any synchronization primitive protecting it. The absence of a mutex or equivalent mechanism means that concurrent reads, writes, and deletes on the map can trigger Go runtime panics and may allow a discharge token to be consumed more than once before deletion completes.
Details
When a user authenticates through the local login flow, a discharge token is generated and stored in a plain map[string]string field named userTokens. The form handler writes to this map when authentication succeeds, and the third-party caveat checker reads from and deletes from the same map when a discharge request arrives. Both code paths execute inside goroutines dispatched by the HTTP server, meaning concurrent requests will access the map simultaneously.
Go's runtime detects concurrent map access and will terminate the process with a fatal error when a write races with another write or read. This makes the API server susceptible to a denial-of-service attack from any authenticated user who can trigger simultaneous discharge requests. Beyond the crash scenario, the read-then-delete sequence in the caveat checker is not atomic. Two goroutines processing the same token concurrently may both pass the existence check before either executes the deletion, allowing a single-use discharge token to be accepted more than once and effectively replaying authentication.
The struct definition that introduces the unsafe field is shown below.
go type localLoginHandlers struct { authCtxt authContext userTokens map[string]string }
The concurrent access originates from the caveat checker calling username, ok := h.userTokens[tokenString] followed by delete(h.userTokens, tokenString) with no lock held, while formHandler concurrently executes h.userTokens[token] = username in a separate goroutine.
PoC
go package main
import ( "net/http" "sync" )
func main() { token := "acquired-discharge-token" endpoint := "https://target-juju-api:17070/local-login/discharge"
var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func() { defer wg.Done() req, := http.NewRequest("GET", endpoint+"?token="+token, nil) http.DefaultClient.Do(req) }() } wg.Wait() }
Impact
Any authenticated user who obtains a valid discharge token can send a burst of concurrent requests to the discharge endpoint. The most reliable outcome is a Go runtime panic caused by concurrent map access, which terminates the Juju API server process and denies service to all connected clients and agents. Under favorable timing conditions the same token may be accepted by multiple goroutines before deletion, bypassing the single-use enforcement and allowing repeated authentication with a token that should have been invalidated after first use.
Summary It is possible that a compromised workload machine under a Juju controller can read any log file for any entity in any model at any level.
There is a debug log endpoint in the API server that allows streaming of logs off of the controller. To access this endpoint you must be authentication and either be a machine agent, controller agent, controller admin or have model read permission.
The problematic is the machine agent story. The rest of the other checks have a high enough degree of safety that an attacker can not move side ways in the controller when obtaining log files.
Details A compromised workload machine is capable of obtaining logs for both the controller and any model under the controller at any log level they wish. A bad actor can use this information as signal for further attacks or possible gain secret information leaked out in debug and trace logs. On top of this they would also be able to receive the logs from the charm itself for which we have no control over.
- here is where the authorizer is defined for the endpoint. - here is where the authorizer is checked. - here and onwards is the amount of information the attacker can gain access to.
PoC
If an attacker compromises a workload machine, they will have access to the agent.conf file containing the credentials. This can then be used to obtain debug logs for any part of the controller.
Summary
Predictable secret ID and lack of secret origin API enable confused deputy attacks on Juju workloads.
Details
A Juju application can create a secret and grant it to another integrated application (grantee).
When they do so, the secret owner has to communicate the secret id to the grantee.
The grantee, having received the secret id can load the secret content and perform operations on behalf of the secret owner.
However, today the grantee has no way to determine which granted secret belongs to which owner.
Instead the grantee relies on: - being able to read the secret by id (secret was in fact granted, by some entity) - secret id was received over a relation (the remote end of the relation is presumed to be secret owner)
Additionally, secret IDs are XID, which are predictable, here two secrets created by two distinct apps in the same K8s model close in time: d34vsl7mp25c76301hs0 time (UTC): 2025-09-17 00:18:28 (Unix 1758068308) machine: f6c88a pid: 50072 counter: 6294648
d34vslfmp25c76301hsg time (UTC): 2025-09-17 00:18:29 (Unix 1758068309) machine: f6c88a pid: 50072 counter: 6294649
PoC
This allows for an IDOR attack where: - actors: - a Good application (the owner of the Victim), - an Evil application, and - a Provider application (the Confused Deputy) - relations: Good --- Provider, Evil --- Provider - secrets: Good and Evil create Secrets, granting them to the Provider and communicate Secret IDs with the Provider. - semantics: the Provider performs some operation on behalf of the Good/Evil using the Secret. - weakness 1: Evil can guess the Secret ID that Good granted and communicated to Provider. - weakness 2: Juju doesn't provide the Provider application the facility to verify the provenance of the Secret IDs. - exploit: Evil passes Good's secret id to Provider. - bypass: Provider performs evil operation with Good's Secret ID on behalf of Evil.
Evil could benefit by: - exfiltrating Good's Secret via reflection. - reading or mutating Good's resources accessible via Good's Secret.
Impact
This requires a complex setup.
Not all shared secrets are used like above, so an actual exploit requires a very specific relation interface, specific semantics of the data in the databag, and an administrator having a reasonable need to deploy two apps (one evil, one good) related to the same (third) provider app.
If exploited, it can be very hard to determine what went wrong after the fact.
Suggested remediation
1. Longer, random secret IDs
For example, if the secret id was extended with a 128-bit nonce, guessing a sibling secret ID would be infeasible, and an attack of this style would require another weakness (e.g. secret IDs exposed in logs)
2. Grantee secret API
Today, an app is not allowed to call secret-info-get on the granted secret. Additionally, granted secrets are not included in the secret-ids output.
Suppose that the Provider could run these hook tools: command (provider/0)> secret-ids my-own-secret-123
(provider/0)> secret-ids --grants good-secret-id-42 evil-secret-id-43
(provider/0)> secret-info-get good-secret-id-42 good-secret-id-42: revision: 1 label: "" owner: good grant-relation-id: 12 rotation: never
The Provider would then able to validate the secret ID it's about to use against: - the relation in which the secret ID has been passed (good relation 12 or evil relation 14) - the application or unit name of the secret owner (good or evil)
A race condition in the secrets management subsystem of Juju versions 3.0.0 through 3.6.18 allows an authenticated unit agent to claim ownership of a newly initialized secret. Between generating a Juju Secret ID and creating the secret's first revision, an attacker authenticated as another unit agent can claim ownership of a known secret. This leads to the attacking unit being able to read the content of the initial secret revision.
Impact Any user with a Juju account on a controller can read debug log messages from the /log endpoint. No specific permissions are required - it's just sufficient for the user to exist in the controller user database. The log messages may contain sensitive information.
Details
The /log endpoint is accessible at the following endpoints: - wss://<controller-ip>/log - wss://<controller-ip>/model/<model-uuid>/log
In order to connect to these endpoints, the client must pass an X-Juju-Client-Version header that matches the current version and pass credentials in a Basic Authorization header. Once connected, the service will stream log events even though the user is not authorised to view them.
To reproduce:
juju bootstrap juju add-user testuser juju change-user-password testuser Run the wscat command below to connect to wss://<controller-ip>:17070/api. Update the JSON payload to include the username and password that were created above.
wscat --no-check -c wss://contorller-ip:17070/model/modelUUID/api { "type": "Admin", "request": "Login", "version": 3, "params": { "client- version": "3.6.1.0", "auth-tag": "user-testuser", "credentials": " password" } }
Observe that the connection fails due to a lack of permissions.
Run the command below to connect to the log endpoint. Note that the credentials are passed in the --auth flag.
wscat --auth user-testuser:password -H "X-Juju-ClientVersion: 3.6.4" --no-check -c wss://<controller-ip>:17070/log
Observe that the logs are returned in the server’s response.
Code
The /log handlers are registered here https://github.com/juju/juju/blob/3.6/apiserver/apiserver.go#L867 https://github.com/juju/juju/blob/3.6/apiserver/apiserver.go#L980
And the only auth required is that the incoming request be for an authenticated user
https://github.com/juju/juju/blob/3.6/apiserver/apiserver.go#L713
but no specific permission checks are done.
Workarounds There are no workarounds.
References F-01
Summary
Certs generated by v4 contain their private key.
Details
Background
Recently, I encountered an API in Go that’s easy to misuse: sha512.Sum384 and sha512.New384().Sum look very similar and behave very differently. https://go.dev/play/p/kDCqqoYk84k demonstrates this. I want to discuss extending static analysis to detect this case with the go community, but before I do that, I want to make a best-effort pass at open-source projects to fix the existing bugs. I figured that if there were any vulnerabilities out there, they would be easy to find once that discussion begins, so it’s better to address them early.
This work is a hobby project and has no affiliation with my employer, so I may be slow to respond due to existing commitments.
PoC
https://go.dev/play/p/vSW0U3Hq4qk
Impact
This code (cert.NewLeaf) generates certs with the SubjectKeyId set to sha512.New384().Sum(/ private / key).
If a cert which was generated by cert.NewLeaf is transferred over the network in plaintext, as is often the case in TLS handshakes, an attacker listening on that network may sniff the cert and trivially extract the private key from it. This applies to client and server TLS certs generated by vulnerable versions of this library.
Getting the server cert and its key would only require performing a TLS handshake (with a matching SNI) with the server. At that point, the attacker could impersonate the server.
Similarly, getting the client cert and its key would require getting the client to perform a TLS handshake against an attacker-controlled server. At that point, an attacker could impersonate the client.