Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
de86bdd
Match SSO rules against the clean path only
rthellend Oct 7, 2026
52261d8
Bind trusted issuer keys to their issuer
rthellend Oct 7, 2026
2bd4b32
Reject invalid SSH certificate TTLs
rthellend Oct 7, 2026
e79e68d
Fix infinite recursion in QUICConn
rthellend Oct 7, 2026
c670893
Check the Host header before authenticating requests
rthellend Oct 7, 2026
dabecfe
Verify OCSP responder authorization and freshness
rthellend Oct 7, 2026
7fd766c
Restrict DNS names in PKI server certificates
rthellend Oct 7, 2026
9a42de0
Add timeouts and a connection limit to the plain HTTP server
rthellend Oct 7, 2026
1284c6e
Limit pending OIDC requests and vacuum less often
rthellend Oct 7, 2026
3ca71b6
Check URL token hosts on login and logout, add expiration
rthellend Oct 7, 2026
30839d6
Use absolute URLs when redirecting to the current request
rthellend Oct 7, 2026
5560b7d
Bind SAML requests to the browser
rthellend Oct 7, 2026
4d5e2c9
Record ECH events only for known server names
rthellend Oct 7, 2026
281a3aa
Strip spoofable forwarding and identity headers
rthellend Oct 7, 2026
e29af4d
Check local handler scopes when no SSO rule matches
rthellend Oct 7, 2026
02e2ced
Bound pending login state in identity providers
rthellend Oct 7, 2026
6346ae0
Check clientData.origin in passkey assertions
rthellend Oct 7, 2026
f2d4946
Bind OIDC authorization requests to the requesting user
rthellend Oct 7, 2026
473a749
Show the client and scopes on the device verification page
rthellend Oct 7, 2026
1feb33d
Make fake passkey credential IDs look real
rthellend Oct 7, 2026
ffe5a14
Fix nil pointer dereference on /.sso/ without SSO
rthellend Oct 7, 2026
13c63e1
Fix stale CRLs and a data race in the built-in PKI
rthellend Oct 7, 2026
0f5dc01
Skip the CSRF check only for valid bearer tokens
rthellend Oct 7, 2026
143e323
Compare OIDC client secrets in constant time
rthellend Oct 7, 2026
0489863
Check redirect_uri and PKCE at the OIDC token endpoint
rthellend Oct 7, 2026
565384a
Mark email_verified finding as declined
rthellend Oct 7, 2026
46af659
Check the WebAuthn signature counter
rthellend Oct 7, 2026
2355c29
Close connections when their client cert is revoked
rthellend Oct 7, 2026
f2ed292
Reject DSA keys in the SSH CA
rthellend Oct 7, 2026
0c5b828
Don't hold the SSH CA lock while reading requests
rthellend Oct 7, 2026
20dfef2
Verify OCSP responses against the verified chain in tlsclient
rthellend Oct 7, 2026
194c18b
Warn when the passphrase is passed on the command line
rthellend Oct 7, 2026
1887023
Quote client-provided SNI and ALPN in QUIC logs
rthellend Oct 7, 2026
74d4aed
Limit connection-level resource use
rthellend Oct 7, 2026
5a00413
Mark MemProfileRate finding as declined
rthellend Oct 7, 2026
791ebc5
Fix data race on the ECH keys
rthellend Oct 7, 2026
8567966
Don't overwrite the request in the OIDC callback handler
rthellend Oct 7, 2026
97061f9
Redact more secrets from the metrics page config
rthellend Oct 7, 2026
1251ba0
Only send the CSRF token to the same origin in proxy.mjs
rthellend Oct 7, 2026
d382785
Don't match group names against emails in ACLs
rthellend Oct 7, 2026
e4e791d
Mark OCSP delegate IsCA finding as not needing a change
rthellend Oct 7, 2026
4e6783e
Mark PKI owner=all finding as declined
rthellend Oct 7, 2026
ff8fdbb
Bound the ephemeral certificate manager's cost per name
rthellend Oct 7, 2026
e62ea25
Don't let backends set the proxy's cookies
rthellend Oct 7, 2026
445c659
Remove the security review TODO list
rthellend Oct 7, 2026
3d6ddbb
Add release notes for the security fixes
rthellend Oct 7, 2026
aa43f6e
Fix the remaining data race on the ECH keys
rthellend Oct 7, 2026
5020b84
Don't charge the forward rate limit twice for QUIC streams
rthellend Oct 7, 2026
e412f2d
Undo certmanager optimization
rthellend Oct 7, 2026
f4db193
Change urltoken exp to a week
rthellend Oct 7, 2026
9613fae
Fix X-Forwarded-For and X-Forwarded-Client-Cert header overrides
rthellend Oct 7, 2026
6a45c0c
Make the certmanager cache size settable for tests
rthellend Oct 7, 2026
c297760
Address review findings
rthellend Oct 7, 2026
86ba943
Use "next" placeholder for changelog
rthellend Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,123 @@
# TLSPROXY Release Notes

## next

This release fixes a number of security issues found during a security review. Some of the fixes change the
behavior of the proxy in ways that may require config changes. Please read the breaking changes below before
upgrading.

### :warning: Breaking changes

* **PKI server certificates must be allowed explicitly.** Users can no longer request server certificates for
arbitrary DNS names. The new `serverCertificates` option of `pki` lists the DNS names (or one-label wildcards)
that can be requested, and optionally which users or groups can request them. Without it, only client
certificates can be issued. For example:
```yaml
pki:
- name: "EXAMPLE CA"
endpoint: https://pki-internal.example.com/certs
serverCertificates:
- dnsNames:
- "*.internal.example.com"
acl:
- bob@example.com
```
* **`jwksUri` must be an https URL** in `trustedIssuers`.
* **Bearer tokens and ID token cookies must have an `exp` claim.** Tokens issued by TLSPROXY always have one.
* **The Host header must match one of the backend's `serverNames`.** This was already enforced for `HTTP` and
`HTTPS` backends, but only after authentication. It is now checked first, and also for `LOCAL` and `CONSOLE`
backends. Requests with a different Host header get `421 Misdirected Request`.
* **Forwarded headers are set by the proxy.** `X-Forwarded-Host` and `X-Forwarded-Proto` are now set on forwarded
requests, and the client's `Forwarded`, `X-Forwarded-Host`, and `X-Forwarded-Proto` headers are removed. Client
headers with the same name as `X-Forwarded-For`, `X-Forwarded-Client-Cert`, `X-tlsproxy-user-id`, or any
`forwardHttpHeaders` key, ignoring case and with `_` instead of `-` (e.g. `X_tlsproxy_user_id`), are removed.
`forwardHttpHeaders` can still override or remove `X-Forwarded-Host` and `X-Forwarded-Proto`.
* **Backends can't set TLSPROXY's cookies.** `Set-Cookie` headers from backends for `TLSPROXYAUTH`,
`TLSPROXYIDTOKEN`, `TLSPROXYNONCE`, `TLSPROXYSAMLNONCE`, or `__tlsproxySid` are dropped.
* **SSH certificate authority:**
* DSA keys are rejected.
* A `ttl` that is not a positive number of seconds is rejected with `400 Bad Request`. Larger values are still
capped at `maximumCertificateLifetime`.
* **Login links expire.** The links used on the login, logout, and permission denied pages expire after a week,
and must point to the same backend.
* **Device authorization page.** The device verification page now has two steps: after entering the user code,
the page shows which client is requesting access and with which scopes, before the user can approve or deny the
request.
* **Local OIDC server:**
* The `redirect_uri` sent to the token endpoint must be the same as in the authorization request.
* When the client sends a PKCE `code_challenge`, the token request must include a matching `code_verifier`.
* An authorization request must be approved by the same user who made it.
* **Passkeys:**
* The origin of passkey assertions is checked, like it already was for registrations.
* Authenticators with a signature counter must increase it on every login. Authenticators that don't implement
a counter are not affected.
* **Connection limits:**
* The TLS handshake timeout is reduced from 2 minutes to 30 seconds.
* Connections and requests that would wait more than 30 seconds for `forwardRateLimit` are rejected instead of
waiting indefinitely.
* Each stream of a non-HTTP/3 QUIC connection counts toward `forwardRateLimit`.
* The plain HTTP server (`httpAddr`) has 10-second timeouts, and accepts at most 1000 concurrent connections, or
`maxOpen` if it is lower.
* At most 50000 pending logins are kept per identity provider, and the local OIDC server keeps at most 10000
pending authorization or device authorization requests. URLs longer than 4096 bytes can't be used as the
destination of a login.
* **ACLs:** an ACL entry that is the name of a group is no longer also compared to the user's email address.
* **`tlsclient --ocsp`** verifies OCSP responses against the verified certificate chain, and rejects responses
signed by certificates that aren't authorized to sign OCSP responses.

### :lock: Security fixes

* SSO rules and exceptions were matched against the raw request path. A path like `/public/../admin` matched an
exception for `/public/`, and was then forwarded as `/admin` without authentication.
* Bearer tokens signed by any trusted issuer's key were accepted as if they had been issued by TLSPROXY.
* The SSH certificate authority issued certificates that never expire when given a negative or very large `ttl`.
* Forwarding a request to a backend with HTTP/3 crashed the proxy.
* ID tokens issued for one backend could be used on another backend with a different Host header.
* OCSP responses, stapled or fetched, could be signed by any certificate issued by the same CA, including the
revoked certificate itself. Fetched responses weren't checked for the serial number or freshness.
* Any PKI user could get a server certificate for any DNS name.
* The plain HTTP server had no timeouts, and idle connections could use up all the available file descriptors.
* Unauthenticated device authorization requests could make the local OIDC server very slow.
* Open redirects after login and after setting the ID token cookie.
* SAML responses weren't tied to the browser that started the login, which allowed an attacker to log a victim
into the attacker's account.
* Client-supplied headers could spoof the user identity header for CGI-style backends.
* The scopes required by built-in endpoints (pki, ssh, etc) weren't checked when no SSO rule matched the path.
* The CSRF check was skipped for any request with an `Authorization` header, even when cookies were used for
authentication.
* Passkey logins were accepted from other origins, and the existence of passkeys for an email address could be
detected.
* A user could approve another user's OIDC authorization request and get tokens despite the client's ACL.
* Revoking a certificate issued by the built-in PKI didn't close connections already using it.
* The built-in PKI could serve a CRL that didn't include the latest revocations.
* Memory use could be increased without limit with ECH client hellos with arbitrary server names, and with
pending login requests.

### :star2: New features

* New `maxOpenPerIP` option to limit the number of open connections from a single client IP address. There is no
limit by default.
* New `serverCertificates` option for `pki` (see above).
* The local OIDC server supports PKCE (RFC 7636), and advertises `code_challenge_methods_supported`.

### :wrench: Misc

* Hardening:
* Constant-time comparison of OIDC client secrets.
* Client-provided server names are quoted in QUIC logs.
* More secrets are redacted from the config shown on the metrics page: static `forwardHttpHeaders` values and
ECH webhook URLs.
* `proxy.mjs` only sends the CSRF token to the same origin.
* A warning is logged when `--passphrase` is used. `$TLSPROXY_PASSPHRASE` should be used instead.
* Bug fixes:
* Data races on the ECH keys and in the PKI's OCSP/CRL signing certificate rotation.
* Panic on `/.sso/` for backends without SSO.
* Possible panic when re-authorizing connections after a backend was removed.
* The session chain claim (`th`) wasn't carried over after an OIDC login.
* The ephemeral certificate manager (for testing) keeps a bounded number of certificates in memory, and reissues
expired certificates.
* New tests for all the changes above.

## v0.25.12

### :wrench: Misc
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ The main configuration options are:
* `hwBacked`: (Optional) Boolean. Enables hardware-backed cryptographic keys (e.g., with a TPM).
* `cacheDir`: (Optional) String. Directory for storing TLS certificates, OCSP responses, etc. Defaults to a system cache directory.
* `defaultServerName`: (Optional) String. Server name to use when SNI is not provided by the client.
* `maxOpen`: (Optional) Integer. Maximum number of open incoming connections. Defaults to half of the open file limit.
* `maxOpenPerIP`: (Optional) Integer. Maximum number of open incoming connections from a single client IP address. No limit by default. Keep in mind that many clients can share one IP address, e.g. behind NAT.
* `logFilter`: (Optional) Object. Controls what gets logged (connections, requests, errors).
* `groups`: (Optional) List of `Group` objects. Defines user groups for access control.
* `backends`: (Required) List of `Backend` objects. Defines the services TLSPROXY will forward traffic to.
Expand Down Expand Up @@ -236,7 +238,7 @@ Each `Backend` object defines a service and its behavior:
* `forwardServerName`: (Optional) String. ServerName to send in TLS handshake with backend.
* `forwardRootCAs`: (Optional) List of strings. CA names or PEM-encoded certificates for backend verification.
* `forwardTimeout`: (Optional) Duration. Connection timeout to backend servers.
* `forwardHttpHeaders`: (Optional) Map of strings. HTTP headers to add to forwarded requests.
* `forwardHttpHeaders`: (Optional) Map of strings. HTTP headers to add to forwarded requests. Headers with the same names sent by clients are removed, including variants with `_` instead of `-`. `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto` are always set by the proxy, and `Forwarded` is removed. They can be overridden here.
* `forwardECH`: (Optional) Object. ECH parameters for connecting to the backend.
* `pathOverrides`: (Optional) List of `PathOverride` objects. Defines different backend parameters for specific path prefixes.
* `proxyProtocolVersion`: (Optional) String. Enables PROXY protocol on this backend (`v1` or `v2`).
Expand Down Expand Up @@ -343,7 +345,7 @@ In this example:
The `TrustedIssuer` object defines an external identity provider whose tokens are accepted by `tlsproxy`. This is useful for distributed authentication where multiple proxies trust each other's user identity tokens.

* `issuer`: (Required) String. The expected "iss" claim value (e.g., "https://auth.example.com/").
* `jwksUri`: (Required) String. The URL to fetch the JSON Web Key Set (JWKS).
* `jwksUri`: (Required) String. The `https` URL to fetch the JSON Web Key Set (JWKS).

**Example:**

Expand Down Expand Up @@ -489,6 +491,9 @@ Defines a local Certificate Authority:
* `ocspServers`: List of strings. URLs for OCSP.
* `endpoint`: String. URL for certificate management.
* `admins`: List of strings. Users allowed to perform administrative tasks.
* `serverCertificates`: (Optional) List of objects. Defines which DNS names users can request in server certificates. Without it, only client certificates can be requested.
* `dnsNames`: List of strings. DNS names (e.g. `foo.example.com`) or wildcards (e.g. `*.example.com`). A wildcard matches exactly one label.
* `acl`: (Optional) List of strings. Users or groups who can request these names. If not set, all users with access to the `endpoint` can.

### SSH Certificate Authority Configuration (`ConfigSSHCertificateAuthority`)

Expand Down Expand Up @@ -573,7 +578,7 @@ To run TLSPROXY, use the `tlsproxy` executable with the `--config` flag pointing

* `--config <file>`: Specifies the path to the configuration YAML file.
* `--revoke-all-certificates <reason>`: Revokes all cached certificates. `reason` can be `unspecified`, `keyCompromise`, `superseded`, or `cessationOfOperation`.
* `--passphrase <passphrase>`: The passphrase to encrypt TLS keys on disk. Can also be set via `TLSPROXY_PASSPHRASE` environment variable.
* `--passphrase <passphrase>`: The passphrase to encrypt TLS keys on disk. Prefer the `TLSPROXY_PASSPHRASE` environment variable: command line arguments are visible to other users on the host.
* `--shutdown-grace-period <duration>`: Graceful shutdown period (e.g., `1m`, `30s`).
* `--use-ephemeral-certificate-manager`: (For testing) Uses an ephemeral certificate manager.
* `--stdout`: Logs output to STDOUT.
Expand Down Expand Up @@ -708,6 +713,12 @@ pki:
# Optional: Admins can revoke anybody's certificates.
admins:
- bob@example.com
# Optional: Allow server certificates for some DNS names.
serverCertificates:
- dnsNames:
- "*.internal.example.com"
acl:
- bob@example.com
```

This example configures a local Certificate Authority (CA) named "EXAMPLE CA". This CA can be used to issue X.509 certificates for client and backend authentication within your environment.
Expand All @@ -718,6 +729,7 @@ Here is a breakdown of the configuration:
* `ocspServers`: Provides the endpoint for the Online Certificate Status Protocol (OCSP), offering a real-time method for checking certificate validity.
* `endpoint`: Sets up a web interface at `https://pki-internal.example.com/certs` where authenticated users can request and manage their own certificates.
* `admins`: Grants administrative privileges to `bob@example.com`, allowing this user to perform actions like revoking any user's certificate.
* `serverCertificates`: Allows `bob@example.com` to request server certificates for names like `foo.internal.example.com`. Without this, users can only request client certificates.

You can then use this CA to enforce client certificate-based authorization for a backend. In the following example, only clients presenting a valid certificate issued by "EXAMPLE CA" for the identity `user@example.com` are allowed access.

Expand Down
58 changes: 58 additions & 0 deletions certmanager/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// MIT License
//
// Copyright (c) 2026 TTBT Enterprises LLC
// Copyright (c) 2026 Robin Thellend <rthellend@rthellend.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// Package certmanager implements an X509 certificate manager that can replace
// https://pkg.go.dev/golang.org/x/crypto/acme/autocert#Manager for testing
// purposes.
// This certificate manager is a self-signed certificate authority that is not
// and should not be trusted for securing any real life communication.
package certmanager

import (
"fmt"
"testing"
)

func TestGetCertCache(t *testing.T) {
cm, err := New("test", t.Logf)
if err != nil {
t.Fatalf("New: %v", err)
}
// Generating keys is slow. Use a small cache.
cm.maxCachedCerts = 5
c1, err := cm.GetCert("foo.example.com")
if err != nil {
t.Fatalf("GetCert: %v", err)
}
if c2, err := cm.GetCert("foo.example.com"); err != nil || c2 != c1 {
t.Errorf("GetCert didn't return cached cert: %v", err)
}
for i := range cm.maxCachedCerts + 1 {
if _, err := cm.GetCert(fmt.Sprintf("n%d.example.com", i)); err != nil {
t.Fatalf("GetCert: %v", err)
}
}
if n := len(cm.certs); n > cm.maxCachedCerts {
t.Errorf("len(certs) = %d", n)
}
}
12 changes: 11 additions & 1 deletion certmanager/certmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ type CertManager struct {

mu sync.Mutex
certs map[string]*tls.Certificate
// maxCachedCerts is the maximum number of certificates kept in memory.
maxCachedCerts int
}

// defaultMaxCachedCerts is the default value of maxCachedCerts.
const defaultMaxCachedCerts = 1000

// New returns a new ephemeral certificate manager.
func New(name string, logger func(string, ...interface{})) (*CertManager, error) {
if logger == nil {
Expand Down Expand Up @@ -107,6 +112,8 @@ func New(name string, logger func(string, ...interface{})) (*CertManager, error)
pool: pool,
logger: logger,
certs: make(map[string]*tls.Certificate),

maxCachedCerts: defaultMaxCachedCerts,
}, nil
}

Expand Down Expand Up @@ -234,9 +241,12 @@ func (cm *CertManager) GetCert(name string) (*tls.Certificate, error) {
}
cm.mu.Lock()
defer cm.mu.Unlock()
if c := cm.certs[name]; c != nil {
if c := cm.certs[name]; c != nil && time.Now().Before(c.Leaf.NotAfter.Add(-5*time.Minute)) {
return c, nil
}
if len(cm.certs) >= cm.maxCachedCerts {
clear(cm.certs)
}

cm.logger("[%s] GetCert(%q)", cm.name, name)
key, err := rsa.GenerateKey(rand.Reader, 2048)
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ func main() {
log.Fatal("--config must be set")
}
log.Printf("INF tlsproxy %s %s %s/%s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
flag.Visit(func(f *flag.Flag) {
if f.Name == "passphrase" {
log.Print("WRN --passphrase is visible to other users on this host. Use $TLSPROXY_PASSPHRASE instead.")
}
})
cfg, err := proxy.ReadConfig(*configFile)
if err != nil {
log.Fatalf("ERR %v", err)
Expand Down
Loading
Loading