cloudflare / cloudflare/pingora

Feat: Server TLS Certificate bundle + SNI based resolver

Open
#594 10 comments 4 reactions 0 assignees View on GitHub
enhancement
Dominant language
Rust
Stars
27.4k
Forks
1.7k
Avg merge
6h 22m
Merged PRs (30d)
3

Description

## What is the problem your feature solves, or the need it fulfills?

First of all, I started taking a look at `pingora` over the weekend and it looks awesome! Thanks so much for open-sourcing it!

What I want to achieve (and already did during testing) is to have the ability to provide multiple TLS certificates and make it possible to automatically choose the correct certificate based on the SNI from the `ClientHello`.

This would make it possible to use `pingora` as a main gateway with a single IP while combining multiple different domains. For instance, if you only have a single IP, you can serve literally anything from your backend purely based on SNI. As soon as then #567 is merged, this combination will be super powerful and you can not only choose upstream servers based on SNI, but also serve different certificates.

## Describe the solution you'd like

I already implemented a working solution based on `rustls`, but I did not open a PR out of the blue, because I wanted to check back with you, if you would be open for something like this. It did not need that many changes and it actually works fine after local testing. You can take a look at the branch [here](https://github.com/sebadob/pingora/tree/rustls_multi_cert_resolver) or I could also open a PR from this.

Basically, what I did is I changed `pingora-core/src/listeners/tls/rustls/mod.rs::TlsSettings` and made it possible to select either a single cert + key (like it is now by default), or optionally provide a certificate + key + SNI bundle, which then will create an SNI based Resolver.

## Describe alternatives you've considered

Since `pingora` did not make it possible by default (at least not for `rustls`, don't know about `openssl`), there is no real alternative I would be aware of.

## Additional context

Since I already have a working example locally, let me show you what I can do with it.

I did not change the current API on purpose to not introduce unnecessary breaking changes. You can still do

```rust
TlsSettings::intermediate(&tls_cert, &tls_key).unwrap();
```

But, what is also possible now is the following, to provide any number of certificates:

```rust
let certs = vec![
BundleCert {
sni: "test1.local".to_string(),
cert_path: "tls/test1/cert-chain.pem".to_string(),
key_path: "tls/test1/key.pem".to_string(),
},
BundleCert {
sni: "test2.local".to_string(),
cert_path: "tls/test2/cert-chain.pem".to_string(),
key_path: "tls/test2/key.pem".to_string(),
},
];
let mut tls_settings = TlsSettings::intermediate_bundle(certs).unwrap();
```

This will now create a `rustls::server::ResolvesServerCertUsingSni` which adds all the provided bundle Certificates. Depending on the SNI from the `ClientHello`, a matching a certificate will be chosen automatically. This works nicely:

```bash
❯ openssl x509 -noout -ext subjectAltName -in tls/test1/cert-chain.pem
X509v3 Subject Alternative Name:
DNS:localhost, DNS:test1.local

❯ openssl x509 -noout -ext subjectAltName -in tls/test2/cert-chain.pem
X509v3 Subject Alternative Name:
DNS:localhost, DNS:test2.local
```

Which then makes the handshakes work for both DNS names

**test1.local:**

```bash
❯ openssl s_client -connect localhost:8080 -servername test1.local
Connecting to 127.0.0.1
CONNECTED(00000003)
depth=2 CN=Nioca Root
verify error:num=19:self-signed certificate in certificate chain
verify return:1
depth=2 CN=Nioca Root
verify return:1
depth=1 CN=Nioca Intermediate
verify return:1
depth=0 CN=localhost, O=rerox test
verify return:1
---
Certificate chain
0 s:CN=localhost, O=rerox test
i:CN=Nioca Intermediate
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Apr 27 13:16:21 2025 GMT; NotAfter: May 7 13:26:21 2026 GMT
1 s:CN=Nioca Intermediate
i:CN=Nioca Root
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: May 7 16:42:41 2024 GMT; NotAfter: Apr 24 18:52:41 2035 GMT
2 s:CN=Nioca Root
i:CN=Nioca Root
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: May 20 21:04:32 2023 GMT; NotAfter: Apr 19 18:52:32 2055 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIB/zCCAYWgAwIBAgIBAjAKBggqhkjOPQQDAzAdMRswGQYDVQQDDBJOaW9jYSBJ
bnRlcm1lZGlhdGUwHhcNMjUwNDI3MTMxNjIxWhcNMjYwNTA3MTMyNjIxWjApMRIw
EAYDVQQDDAlsb2NhbGhvc3QxEzARBgNVBAoMCnJlcm94IHRlc3QwdjAQBgcqhkjO
PQIBBgUrgQQAIgNiAASIPiRDvYy0SFQ65fop0JquBduPJK9aWflJ8SHUXZe+0PJu
fr0A143cEszWliRL9V6y5K6Tms8lZ5RQPlgY7qfSCjEGZpNGReMCxQe0akwwozZE
5wfIUa/a3amkWg9QiVyjgYwwgYkwHwYDVR0jBBgwFoAUK/pd61lzdkNyi8AcSE8P
ZWpnCzQwIQYDVR0RBBowGIIJbG9jYWxob3N0ggt0ZXN0MS5sb2NhbDATBgNVHSUE
DDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUCmWGZ/YZpMJxX+AqhidGvptralYwDwYD
VR0TAQH/BAUwAwEBADAKBggqhkjOPQQDAwNoADBlAjAMs621n/TVTHidQ6JcPJTD
R+nWbK4zH0XcU30ORFmd/BzgDaLIiCjKS/KkDW1nz8ECMQC+O9QszgLHk/7WbNLu
d2h6QlsgoeE/68DYVlhjxtujsMbbMc1K8DqNQtdkk5Iu5ZQ=
-----END CERTIFICATE-----
subject=CN=localhost, O=rerox test
issuer=CN=Nioca Intermediate
---
No client certificate CA names sent
Peer signing digest: SHA384
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1811 bytes and written 402 bytes
Verification error: self-signed certificate in certificate chain
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 384 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 19 (self-signed certificate in certificate chain)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: B4D119EF274DDA5D07F12BAF11D155B9A6149C26A7FEDDDA280283EAD4A737BA
Session-ID-ctx:
Resumption PSK: 0B8B5D6F040F79E5188FB102B0FF1FAC0241E9389420E852710C2CCA6D98532AD9C94F6B0E7C4858A4171556D3FEC373
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 86400 (seconds)
TLS session ticket:
0000 - 9e a0 2a 48 91 93 f7 29-e0 81 e4 95 b8 bf c9 69 ..*H...).......i
0010 - c6 02 ff da 47 49 a1 c0-9f a3 4c 0c a9 32 c9 fc ....GI....L..2..

Start Time: 1745761853
Timeout : 7200 (sec)
Verify return code: 19 (self-signed certificate in certificate chain)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: FF0D7059CF7F09DE44F73450ED3147E67CF1E46C6AF2796031896ACD6F90A844
Session-ID-ctx:
Resumption PSK: 12BE27EFC34B867B616A9E6B8DD200EEB9FF6C2C5A58D0D50CDC286A9DE38F395B5A04B2CE14B078DA334E41BF744543
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 86400 (seconds)
TLS session ticket:
0000 - 62 73 bf 24 98 79 8d fa-ee 17 55 f8 73 cd 1c 40 bs.$.y....U.s..@
0010 - cd 3f 0e 49 97 44 1a a8-39 4c 55 a8 2f 14 a8 98 .?.I.D..9LU./...

Start Time: 1745761853
Timeout : 7200 (sec)
Verify return code: 19 (self-signed certificate in certificate chain)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
```

**test2.local:**

```bash
❯ openssl s_client -connect localhost:8080 -servername test2.local
Connecting to 127.0.0.1
CONNECTED(00000003)
depth=2 CN=Nioca Root
verify error:num=19:self-signed certificate in certificate chain
verify return:1
depth=2 CN=Nioca Root
verify return:1
depth=1 CN=Nioca Intermediate
verify return:1
depth=0 CN=localhost, O=rerox test
verify return:1
---
Certificate chain
0 s:CN=localhost, O=rerox test
i:CN=Nioca Intermediate
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Apr 27 13:16:56 2025 GMT; NotAfter: May 7 13:26:56 2026 GMT
1 s:CN=Nioca Intermediate
i:CN=Nioca Root
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: May 7 16:42:41 2024 GMT; NotAfter: Apr 24 18:52:41 2035 GMT
2 s:CN=Nioca Root
i:CN=Nioca Root
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: May 20 21:04:32 2023 GMT; NotAfter: Apr 19 18:52:32 2055 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIB/zCCAYWgAwIBAgIBAzAKBggqhkjOPQQDAzAdMRswGQYDVQQDDBJOaW9jYSBJ
bnRlcm1lZGlhdGUwHhcNMjUwNDI3MTMxNjU2WhcNMjYwNTA3MTMyNjU2WjApMRIw
EAYDVQQDDAlsb2NhbGhvc3QxEzARBgNVBAoMCnJlcm94IHRlc3QwdjAQBgcqhkjO
PQIBBgUrgQQAIgNiAASVcThGdDCuIpJtm/8F9n0fkSp8NRpnFn+NLYdJa6jIZdOV
C9xPFNJ7+Akw0irhPk+8ugenNbh5Lfbmpc6OE39zluG7GGCzTXj2QQ/3OTZEKZ+6
UFOv7pbgmA7prCq5FpWjgYwwgYkwHwYDVR0jBBgwFoAUK/pd61lzdkNyi8AcSE8P
ZWpnCzQwIQYDVR0RBBowGIIJbG9jYWxob3N0ggt0ZXN0Mi5sb2NhbDATBgNVHSUE
DDAKBggrBgEFBQcDATAdBgNVHQ4EFgQU5MFJ32uXU0FH+QcrFwdlI7usqigwDwYD
VR0TAQH/BAUwAwEBADAKBggqhkjOPQQDAwNoADBlAjEAotRKC3BmzT0oZ0uNfoRW
H4mUvVxqEgUriMVZ5sFcoOIXLAg2eUP1uWVntEKQX1MgAjBv5F4gu/LsMZWFHmfC
CltpnlLgMVG0wHZGhX9m0HN4KUmZIVtHHvMVK0JV40vhCso=
-----END CERTIFICATE-----
subject=CN=localhost, O=rerox test
issuer=CN=Nioca Intermediate
---
No client certificate CA names sent
Peer signing digest: SHA384
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 1811 bytes and written 402 bytes
Verification error: self-signed certificate in certificate chain
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 384 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 19 (self-signed certificate in certificate chain)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 62FE61AAF41893EDF2A501BA494D5A73AB3996ADEFC8110E2A6D027FB3455B80
Session-ID-ctx:
Resumption PSK: 72AAE667644D396E2B6F1DC112569366843B41E6427C6B70CB8B4DFD518D2479E3D52AF277A78D05822586F2BB18C48B
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 86400 (seconds)
TLS session ticket:
0000 - 1e 31 b1 13 32 c8 c4 ed-54 4d 72 76 4b 52 ad 22 .1..2...TMrvKR."
0010 - 80 8a 6a e3 1e 9e 12 5f-bf 3e 23 d1 a3 32 34 3f ..j...._.>#..24?

Start Time: 1745761951
Timeout : 7200 (sec)
Verify return code: 19 (self-signed certificate in certificate chain)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 2D924B1C9F038E26CC2D32DB3B4FA81B12FCEF1D65C859C5C0A5670F6A049AA4
Session-ID-ctx:
Resumption PSK: 40605E2D9A15ADDF4E88C86A2A7803B5CD28D8689E6BFFBAEBBDF88E57859075217AFF989C9D9AD4A57D21A71F4EA63B
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 86400 (seconds)
TLS session ticket:
0000 - 32 cb 26 53 69 8c 6b aa-e0 62 90 f7 0a 50 a4 80 2.&Si.k..b...P..
0010 - 15 f2 0f bd 9f de 03 50-de e5 c2 20 ae 9e 10 32 .......P... ...2

Start Time: 1745761951
Timeout : 7200 (sec)
Verify return code: 19 (self-signed certificate in certificate chain)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
```

And it fails of course for non-valid ones:

```bash
❯ openssl s_client -connect localhost:8080 -servername doesnotexist.local
Connecting to 127.0.0.1
CONNECTED(00000003)
C06203B7457F0000:error:0A000419:SSL routines:ssl3_read_bytes:tlsv1 alert access denied:ssl/record/rec_layer_s3.c:909:SSL alert number 49
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 329 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
```

Let me know what you think of it. I would gladly open a PR from the linked branch, as it is working nicely so far.

Contributor guide

Open the contributing guide

Research direction

Start in pingora-core/src/listeners/tls/rustls/mod.rs, focusing on TlsSettings and the existing single-certificate setup. Review the linked rustls_multi_cert_resolver branch to understand the proposed bundle API and SNI resolver behavior. Done means preserving the current single-certificate API while selecting the matching certificate for each configured SNI name.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
networking, security
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.