TCP tunnel payload is plaintext after the handshake #2

Closed
opened 2026-08-31 18:53:10 -05:00 by Ghost · 1 comment

Severity: High

The control handshake is AES-GCM, but the actual forwarded TCP stream is copied in the clear over the client↔server connection. That contradicts the README (“end-to-end encryption”, “All traffic is encrypted using AES-GCM”) and is how people would actually use this (HTTP, databases, anything that is not already TLS).

What happens

  1. Client handleTCPConnection encrypts only PortForwardRequest, then forwardData copies raw bytes both ways (internal/client/client.go).
  2. Server readRequest decrypts that request, then handleTCPForwardforwardData copies raw bytes to the target (internal/server/server.go).
  3. Anyone who can sniff listen_address (LAN, a VPS NIC, a mis-bound 0.0.0.0:9000) sees the payload after the first length-prefixed blob.

UDP packets are encrypted per datagram. TCP is not.

SSH-over-teleport is still SSH, so that use case is fine. Plain HTTP, Postgres, Redis, SMB, etc. are fully exposed on the tunnel port.

Suggested fix (describe-only, no PR)

Do not use raw io copy on the tunnel socket. Frame the data path the same way as the request:

  • Derive the AES key once at process start (not per packet).
  • For each chunk: encrypt with AES-GCM, write uint32 length + ciphertext.
  • Reader: read length (cap it), decrypt, write plaintext to the local/target conn.
  • Apply this in both forwardData implementations (client and server), both directions.

Until that lands, treat teleport as an unencrypted TCP proxy with an encrypted handshake, and only forward protocols that already encrypt themselves (SSH, HTTPS). Do not advertise database/HTTP forwarding as “secure.”

## Severity: High The control handshake is AES-GCM, but **the actual forwarded TCP stream is copied in the clear** over the client↔server connection. That contradicts the README (“end-to-end encryption”, “All traffic is encrypted using AES-GCM”) and is how people would actually use this (HTTP, databases, anything that is not already TLS). ### What happens 1. Client `handleTCPConnection` encrypts only `PortForwardRequest`, then `forwardData` copies raw bytes both ways (`internal/client/client.go`). 2. Server `readRequest` decrypts that request, then `handleTCPForward` → `forwardData` copies raw bytes to the target (`internal/server/server.go`). 3. Anyone who can sniff `listen_address` (LAN, a VPS NIC, a mis-bound `0.0.0.0:9000`) sees the payload after the first length-prefixed blob. UDP packets *are* encrypted per datagram. TCP is not. SSH-over-teleport is still SSH, so that use case is fine. Plain HTTP, Postgres, Redis, SMB, etc. are fully exposed on the tunnel port. ### Suggested fix (describe-only, no PR) Do not use raw `io` copy on the tunnel socket. Frame the data path the same way as the request: - Derive the AES key **once** at process start (not per packet). - For each chunk: encrypt with AES-GCM, write `uint32` length + ciphertext. - Reader: read length (cap it), decrypt, write plaintext to the local/target conn. - Apply this in both `forwardData` implementations (client and server), both directions. Until that lands, treat teleport as an unencrypted TCP proxy with an encrypted handshake, and only forward protocols that already encrypt themselves (SSH, HTTPS). Do not advertise database/HTTP forwarding as “secure.”

Triaged as high. README overclaims; TCP payload is not AES-GCM. Next: frame+encrypt both forwardData impls. Not into PR #1. Until then only forward SSH/HTTPS. Queued for Ash after #1 is green.

Triaged as high. README overclaims; TCP payload is not AES-GCM. Next: frame+encrypt both forwardData impls. Not into PR #1. Until then only forward SSH/HTTPS. Queued for Ash after #1 is green.
Ghost closed this issue 2026-08-31 22:30:40 -05:00
Sign in to join this conversation.