Authentication mechanism absence or evasion In github.com/canonical/lxd
Description
LXD: Update of type field in restricted TLS certificate allows privilege escalation to cluster admin
Summary
A restricted TLS certificate user can escalate to cluster admin by changing their certificate type from client to server via PUT/PATCH to /1.0/certificates/{fingerprint}. The non-admin guard and reset block in doCertificateUpdate fail to validate or reset the Type field, allowing a caller-supplied value to persist to the database. The modified certificate is matched as a server certificate during TLS authentication, granting ProtocolCluster with full admin privileges.
Details
doCertificateUpdate in lxd/certificates.go handles PUT/PATCH requests to /1.0/certificates/{fingerprint} for both privileged and unprivileged callers. The access handler is allowAuthenticated, so any trusted TLS user (including restricted) can reach this code.
For unprivileged callers (restricted users who fail the EntitlementCanEdit check at line 975), two defenses are intended to prevent field tampering:
The guard block validates that Restricted, Name, and Projects match the original database record. Does not check Type.
// Ensure the user in not trying to change fields other than the certificate. if dbInfo.Restricted != req.Restricted || dbInfo.Name != req.Name || len(dbInfo.Projects) != len(req.Projects) { return response.Forbidden(errors.New("Only the certificate can be changed")) }
The reset block rebuilds the dbCert struct using original values for Restricted, Name, and Certificate. Uses reqDBType (caller-supplied) for Type instead of the original dbInfo type.
// Reset dbCert in order to prevent possible future security issues. dbCert = dbCluster.Certificate{ Certificate: dbInfo.Certificate, Fingerprint: dbInfo.Fingerprint, Restricted: dbInfo.Restricted, Name: dbInfo.Name, Type: reqDBType, }...
This allows the attacker to update the Type field of their own certificate from client to server, bypassing the authorization controls and escalating to cluster admin.
PoC
Tested on lxd 6.7.
As admin, create restricted project and restricted certificate:
# Create restricted project lxc project create poc-restricted -c restricted=true lxc profile device add default root disk path=/ pool=default --project poc-restricted lxc profile device add default eth0 nic network=lxdbr0 --project poc-restricted # Add client certificate lxc config trust add --restricted --projects poc-restricted --name poc-user # pass token to user...
As restricted user:
# Add token lxc remote add target <token> # Confirm we can only see the poc-restricted project lxc project list target: # Confirm we can't unrestrict the project lxc project set target:poc-restricted restricted=false...
Impact
Privilege escalation from restricted TLS certificate user (project-scoped) to cluster admin.
Cluster admin can create privileged containers (security.privileged=true) or pass raw LXC config (raw.lxc), which provides root-level access to the host, leading to full host compromise.
The attack requires a single PUT/PATCH request. The escalation is persistent and takes effect immediately after the identity cache refresh. The change in permissions is not logged.
Affects any LXD deployment using legacy restricted TLS certificates (/1.0/certificates API).
Suggested remediation
Add Type to the guard check at line 992:
if dbInfo.Restricted != req.Restricted || dbInfo.Name != req.Name || dbInfo.Type != req.Type || len(dbInfo.Projects) != len(req.Projects) {
Use the original type in the reset block at line 1008:
origDBType, err := certificate.FromAPIType(dbInfo.Type) if err != nil { return response.InternalError(err) } dbCert = dbCluster.Certificate{ Certificate: dbInfo.Certificate, Fingerprint: dbInfo.Fingerprint,...
Patches
LXD Series | Interim release |
|---|---|
6 | |
5.21 | |
5.0 | |
4.0 |
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
go | - | ||
debian 14 | 6.0.6-3 | ||
debian 13 | 6.0.4-2+deb13u6 | ||
debian 12 | 5.0.2-5+deb12u5 | ||
debian 13 | 5.0.2+git20231211.1364ae4-9+deb13u5 |
Aliases
References