The Macaroon Mirage: Bypassing Juju's Cross-Model Authorization
Jan 29, 2026·7 min read·7 visits
Executive Summary (TL;DR)
Juju controllers failed to reject macaroons signed with unknown keys. Instead of blocking them, the system asked for 'discharges' (3rd party verification), effectively helping attackers validate their own forged tokens. Fix requires patching to explicitly handle 'key not found' errors.
A logic flaw in Canonical Juju's macaroon validation mechanism allows attackers to bypass cross-model authorization. By presenting a forged macaroon signed with an unknown key, an attacker can trick the controller into entering a discharge workflow that inadvertently validates the forged permissions. This vulnerability turns a cryptographic failure into a trusted credential exchange.
The Hook: Sweet, Sweet Macaroons
In the world of distributed systems, cookies are boring. Google gave us Macaroons, and they are objectively cooler. Unlike a static API key or a bearer token, a Macaroon is a 'cookie with caveats'. It carries its own authorization logic. You can take a Macaroon that grants admin access and append a caveat that says ...but only on Tuesdays and ...only from IP 10.0.0.1, then hand it off to a worker node. The worker can't remove those caveats because the signature is chained. It's cryptographic delegation at its finest.
Canonical's Juju uses these Macaroons heavily for Cross-Model Relations (CMR). When you have a database in one model (namespace) and a web app in another, they talk via CMR. The permissions for this are baked into Macaroons.
But here is the thing about cryptography: it relies on trust anchors. If I hand you a Macaroon, you need to verify it against a root key you possess. If you don't have the key, the math fails. In a sane world, if the math fails, you throw the request in the trash. In the world of CVE-2026-1237, Juju decided to ask the attacker for clarification instead. It's the digital equivalent of a bouncer seeing a fake ID drawn in crayon and saying, 'This looks weird, go ask the manager to stamp it.'
The Flaw: The Discharge Trap
The vulnerability lies in the apiserver/common/crossmodel/auth.go file, specifically within the checkMacaroons function. This function utilizes the macaroon-bakery library to validate incoming tokens. When a Macaroon is presented, the bakery attempts to retrieve the root key associated with the token's ID to verify the signature.
Here is where the logic went off the rails. If an attacker mints a Macaroon with a random key (one the Controller doesn't have), the bakery library returns an error: macaroon not found in storage. This is the correct behavior from the library. It is effectively saying, 'I have never seen this token before; it is forged or irrelevant.'
However, the Juju controller didn't treat this error as a hard stop. The developers were likely anticipating a different kind of error: caveat verification failures. In the Macaroon protocol, if a token has 'third-party caveats' (conditions that need checking by another service), the verification fails, and the client is told to go get a 'discharge' token.
Juju's code caught the error from the bakery but didn't distinguish between 'I don't know this key' (Security Breach) and 'I need more proof' (Protocol Flow). It defaulted to the latter. The controller saw the failure, assumed it was a discharge requirement, and initiated a handshake to verify the forged caveats. It implicitly trusted the content of a token it couldn't cryptographically verify.
The Code: String Theory
The fix is surprisingly—and perhaps disturbingly—simple. It relies on string matching against an error message. While effective, it highlights how fragile error handling in Go can be if you aren't using typed errors.
Here is the essence of the vulnerable flow in apiserver/common/crossmodel/auth.go before the patch:
// BEFORE: Blindly processing errors
authInfo, err := d.bakery.CheckMacaroons(ctx, macaroons)
if err != nil {
// The code assumes 'err' means we need a discharge
// It does NOT check if the root key was missing completely
return d.handleDischarge(ctx, err, macaroons)
}The patch (PR #21062) introduces a specific check for the macaroon not found in storage string. If the bakery doesn't know the key, the door is slammed shut immediately.
// AFTER: Trust, but verify the error
const rootKeyErrorMessage = "macaroon not found in storage"
authInfo, err := d.bakery.CheckMacaroons(ctx, macaroons)
if err != nil {
// CRITICAL FIX: explicit check for missing keys
if strings.Contains(errors.Details(err), rootKeyErrorMessage) {
logger.Errorf("rejecting forged macaroon: %v", err)
return nil, apiservererrors.ErrPerm
}
// Only proceed to discharge logic if the key was valid
// but caveats were unsatisfied
return d.handleDischarge(ctx, err, macaroons)
}> [!NOTE] > This is a reminder that library error messages are part of your API contract. If the underlying library changes that error string, this security patch could silently regress.
The Exploit: Minting Your Own Admin Badge
To exploit this, we don't need to steal a key. We just need to make one up. The attack vector is classified as High Complexity (AC:H) because it requires specific network positioning (Adjacent) and the ability to interact with the CMR endpoint, but the logic is straightforward.
Step 1: The Forge
The attacker uses a local Macaroon bakery to mint a new token. They sign it with a random key, let's call it Key_Evil. Inside, they embed caveats claiming they are a privileged user, for example: user == admin or uuid == target-offer-uuid.
Step 2: The Presentation The attacker sends this forged Macaroon to the target Juju controller's API.
Step 3: The Bait
The Juju controller looks up the ID associated with Key_Evil. It finds nothing. The bakery returns macaroon not found. The vulnerable code catches this error. Instead of returning 403 Forbidden, it thinks: "Ah, the user needs to prove these caveats."
Step 4: The Switch
The controller generates a DischargeRequiredError. This is where it gets funny. The controller effectively tells the attacker: "I can't verify this token yet. Go ask the Discharge Service to verify that user == admin."
Step 5: The Loophole The attacker takes this request to the Discharge Service. The service checks the condition. Since the attacker wrote the condition into the forged Macaroon, and the service is just validating if the condition is met (not the validity of the original token root), the discharge is granted.
The attacker combines the original forged Macaroon with the legitimate Discharge token. The Juju controller now accepts the pair, granting access to the cross-model relation. We have successfully turned a database lookup failure into Remote Code Execution (via workload manipulation).
The Impact: Low Score, High Drama
You might notice the CVSS score is a measly 2.1 (Low). Do not let that fool you into thinking this is harmless. The low score comes from the strict requirements of CVSS v4.0 regarding 'Attack Requirements' (AT:P) and 'Network' (AV:A). You generally need to be already inside the network or have some level of access to the Juju environment to fire this off.
However, in a multi-tenant cloud environment where Juju is managing disparate workloads, this is a privilege escalation nightmare. If you are a tenant on a shared controller, you could theoretically leverage this to access offers and services belonging to other tenants that you should strictly be walled off from.
Impacts include:
- Unauthorized Access: Gaining control over cross-model relations you don't own.
- Data Exfiltration: Connecting to database offers (e.g., a shared PostgreSQL charm) without valid credentials.
- Denial of Service: Revoking or modifying relations to break production applications.
It is a classic 'Soft underbelly' vulnerability. Hard to reach, but if you poke it, the whole thing doubles over.
The Mitigation: Patch or perish
Because this is a logic flaw in the compiled Go binary of the Juju controller, there are no config tweaks or firewall rules that can cleanly fix this. You cannot WAF your way out of a Macaroon logic error easily because the payload is opaque.
Immediate Action:
- Upgrade Juju: Ensure you are running a version that includes PR #21062. This likely means the latest patch releases for Juju 3.x.
- Audit Logs: Grep your controller logs for
macaroon not found in storage. In a healthy cluster, this should be rare. If you see bursts of this error followed by successful relation hooks, you might have been probed. - Rotate Keys: If you suspect compromise, rotating the actual Macaroon root keys in the controller database is the nuclear option, though it will disconnect all current agents until they re-authenticate.
For developers using Macaroons in their own Go apps: Learn from this. Always check the error type. Do not assume err != nil simply means 'verification pending'. Sometimes it means 'counterfeit'.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:N/VC:L/VI:L/VA:L/SC:L/SI:L/SA:LAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Juju Canonical | < 3.6 (approx, reliant on PR date) | See Vendor Advisory |
| Attribute | Detail |
|---|---|
| CWE | CWE-672 (Resource After Expiration/Revocation) |
| Attack Vector | Adjacent Network (AV:A) |
| CVSS v4.0 | 2.1 (Low) |
| Privileges Required | Low (PR:L) |
| User Interaction | None (UI:N) |
| Exploit Status | PoC / Theoretical |
MITRE ATT&CK Mapping
The application does not correctly handle the expiration or revocation of a resource, or in this case, the absence of a valid root of trust, allowing continued access.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.