Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for forwarded Tls-Client-Cert #17272

Merged
merged 28 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2ecabd4
Add support for x_forwarded_for_client_cert_header
JasonN3 Sep 22, 2022
8d140f6
add changelog entry
JasonN3 Sep 22, 2022
89bc46d
add tests for a badly and properly formatted certs
JasonN3 Sep 23, 2022
fb1515b
both conditions should be true
JasonN3 Oct 3, 2022
7396430
handle case where r.TLS is nil
JasonN3 Jan 24, 2023
b7a1ffe
prepend client_certs to PeerCertificates list
JasonN3 Jan 24, 2023
7108090
Add support for x_forwarded_for_client_cert_header
JasonN3 Sep 22, 2022
a922d70
add changelog entry
JasonN3 Sep 22, 2022
bf50eab
add tests for a badly and properly formatted certs
JasonN3 Sep 23, 2022
663c7ea
both conditions should be true
JasonN3 Oct 3, 2022
bc2d50b
handle case where r.TLS is nil
JasonN3 Jan 24, 2023
08c2515
prepend client_certs to PeerCertificates list
JasonN3 Jan 24, 2023
e72c2f5
add option for decoders to handle different proxies
JasonN3 May 26, 2023
4761980
Merge branch 'clientcert' of https://github.com/JasonN3/hashicorp-vau…
JasonN3 May 26, 2023
9be26a9
Add support for x_forwarded_for_client_cert_header
JasonN3 Sep 22, 2022
0e6a29a
add changelog entry
JasonN3 Sep 22, 2022
78ed9a9
add tests for a badly and properly formatted certs
JasonN3 Sep 23, 2022
83eaef7
both conditions should be true
JasonN3 Oct 3, 2022
e0cc93e
handle case where r.TLS is nil
JasonN3 Jan 24, 2023
c3d358d
prepend client_certs to PeerCertificates list
JasonN3 Jan 24, 2023
fe19996
add option for decoders to handle different proxies
JasonN3 May 26, 2023
dfdba2d
Merge branch 'clientcert' of https://github.com/JasonN3/hashicorp-vau…
JasonN3 May 26, 2023
9c1d5e1
Merge branch 'main' into pr-17272
cipherboy May 26, 2023
0a7cf21
fix tests
JasonN3 May 26, 2023
3b8c20c
fix typo
JasonN3 Feb 28, 2024
718f1a5
Merge branch 'main' into clientcert
sgmiller Mar 1, 2024
ed530d3
Merge branch 'main' into clientcert
VioletHynes Mar 19, 2024
cc5cd44
Merge branch 'main' into clientcert
JasonN3 Apr 3, 2024
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
3 changes: 3 additions & 0 deletions changelog/17272.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth/cert: Adds support for TLS certificate authenticaion through a reverse proxy that terminates the SSL connection
```
8 changes: 8 additions & 0 deletions command/server/listener_tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ func tcpListenerFactory(l *configutil.Listener, _ io.Writer, ui cli.Ui) (net.Lis
if len(l.XForwardedForAuthorizedAddrs) > 0 {
props["x_forwarded_for_reject_not_authorized"] = strconv.FormatBool(l.XForwardedForRejectNotAuthorized)
}

if len(l.XForwardedForAuthorizedAddrs) > 0 {
props["x_forwarded_for_client_cert_header"] = fmt.Sprintf("%s", l.XForwardedForClientCertHeader)
}

if len(l.XForwardedForAuthorizedAddrs) > 0 {
props["x_forwarded_for_client_cert_header_decoders"] = fmt.Sprintf("%s", l.XForwardedForClientCertHeaderDecoders)
}
}

tlsConfig, reloadFunc, err := listenerutil.TLSConfig(l, props, ui)
Expand Down
78 changes: 78 additions & 0 deletions http/forwarded_for_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package http

import (
"bytes"
"encoding/base64"
"net/http"
"net/url"
"strings"
"testing"

Expand Down Expand Up @@ -255,4 +257,80 @@ func TestHandler_XForwardedFor(t *testing.T) {
t.Fatalf("bad body: %s", buf.String())
}
})

// Next: test an invalid certificate being sent
t.Run("reject_bad_cert_in_header", func(t *testing.T) {
t.Parallel()
testHandler := func(props *vault.HandlerProperties) http.Handler {
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.RemoteAddr))
})
listenerConfig := getListenerConfigForMarshalerTest(goodAddr)
listenerConfig.XForwardedForClientCertHeader = "X-Forwarded-Tls-Client-Cert"
listenerConfig.XForwardedForClientCertHeaderDecoders = "URL,BASE64"
return WrapForwardedForHandler(origHandler, listenerConfig)
}

cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: HandlerFunc(testHandler),
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client

req := client.NewRequest("GET", "/")
req.Headers = make(http.Header)
req.Headers.Set("x-forwarded-for", "5.6.7.8")
req.Headers.Set("x-forwarded-tls-client-cert", `BAD_TEXTMIIDtTCCAp2gAwIBAgIUf%2BjhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL%0ABQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw%0AMTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq%0AhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS%0ATRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn%2B1PswtivhKi%2BeLtgWkUF9cFYFGn%0ASgMld6ZWRhNheZhA6ZfQmeM%2FBF2pa5HK2SDF36ljgjL9T%2BnWrru2Uv0BCoHzLAmi%0AYYMiIWplidMmMO5NTRG3k%2B3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5%0AdonyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf%2FGLcUVG%0AB%2B5%2BAAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7%2BmCzz%2BanqiJfyr2nwIDAQABo4H1%0AMIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm%2B%2Be%0AHpyM3p708bgZJuRYEdX1o%2BUwHwYDVR0jBBgwFoAUncSzT%2F6HMexyuiU9%2F7EgHu%2Bo%0Ak5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x%0AOjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A%0AAAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br%0AaS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy%2BSgMIrwfs%0AX1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4%0AaYqNKFWrRaBRAaaYZ%2FO1ApRTOrXqRx9Eqr0H1BXLsoAq%2BmWassL8sf6siae%2BCpwA%0AKqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU%2BidkuqfV2h1BQKgSEhFDABjFdTCN%0AQDAHsEHsi2M4%2FjRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj%0AxqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc%2FJ9DIQM%2BXmk%3D`)
resp, err := client.RawRequest(req)
if err == nil {
t.Fatal("expected error")
}
defer resp.Body.Close()
buf := bytes.NewBuffer(nil)
buf.ReadFrom(resp.Body)
if !strings.Contains(buf.String(), "failed to base64 decode the client certificate: ") {
t.Fatalf("bad body: %v", buf.String())
}
})

// Next: test a valid (unverified) certificate being sent
t.Run("pass_cert", func(t *testing.T) {
t.Parallel()
testHandler := func(props *vault.HandlerProperties) http.Handler {
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(base64.StdEncoding.EncodeToString(r.TLS.PeerCertificates[0].Raw)))
})
listenerConfig := getListenerConfigForMarshalerTest(goodAddr)
listenerConfig.XForwardedForClientCertHeader = "X-Forwarded-Tls-Client-Cert"
listenerConfig.XForwardedForClientCertHeaderDecoders = "URL,BASE64"
return WrapForwardedForHandler(origHandler, listenerConfig)
}

cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: HandlerFunc(testHandler),
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client

req := client.NewRequest("GET", "/")
req.Headers = make(http.Header)
req.Headers.Set("x-forwarded-for", "5.6.7.8")
testcertificate := `MIIDtTCCAp2gAwIBAgIUf%2BjhKTFBnqSs34II0WS1L4QsbbAwDQYJKoZIhvcNAQEL%0ABQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzQxWhcNMjUw%0AMTA1MTAyODExWjAbMRkwFwYDVQQDExBjZXJ0LmV4YW1wbGUuY29tMIIBIjANBgkq%0AhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZx0Svr82YJpFpIy4fJNW5fKA6B8mhxS%0ATRAVnygAftetT8puHflY0ss7Y6X2OXjsU0PRn%2B1PswtivhKi%2BeLtgWkUF9cFYFGn%0ASgMld6ZWRhNheZhA6ZfQmeM%2FBF2pa5HK2SDF36ljgjL9T%2BnWrru2Uv0BCoHzLAmi%0AYYMiIWplidMmMO5NTRG3k%2B3AN0TkfakB6JVzjLGhTcXdOcVEMXkeQVqJMAuGouU5%0AdonyqtnaHuIJGuUdy54YDnX86txhOQhAv6r7dHXzZxS4pmLvw8UI1rsSf%2FGLcUVG%0AB%2B5%2BAAGF5iuHC3N2DTl4xz3FcN4Cb4w9pbaQ7%2BmCzz%2BanqiJfyr2nwIDAQABo4H1%0AMIHyMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUm%2B%2Be%0AHpyM3p708bgZJuRYEdX1o%2BUwHwYDVR0jBBgwFoAUncSzT%2F6HMexyuiU9%2F7EgHu%2Bo%0Ak5swOwYIKwYBBQUHAQEELzAtMCsGCCsGAQUFBzAChh9odHRwOi8vMTI3LjAuMC4x%0AOjgyMDAvdjEvcGtpL2NhMCEGA1UdEQQaMBiCEGNlcnQuZXhhbXBsZS5jb22HBH8A%0AAAEwMQYDVR0fBCowKDAmoCSgIoYgaHR0cDovLzEyNy4wLjAuMTo4MjAwL3YxL3Br%0AaS9jcmwwDQYJKoZIhvcNAQELBQADggEBABsuvmPSNjjKTVN6itWzdQy%2BSgMIrwfs%0AX1Yb9Lefkkwmp9ovKFNQxa4DucuCuzXcQrbKwWTfHGgR8ct4rf30xCRoA7dbQWq4%0AaYqNKFWrRaBRAaaYZ%2FO1ApRTOrXqRx9Eqr0H1BXLsoAq%2BmWassL8sf6siae%2BCpwA%0AKqBko5G0dNXq5T4i2LQbmoQSVetIrCJEeMrU%2BidkuqfV2h1BQKgSEhFDABjFdTCN%0AQDAHsEHsi2M4%2FjRW9fqEuhHSDfl2n7tkFUI8wTHUUCl7gXwweJ4qtaSXIwKXYzNj%0AxqKHA8Purc1Yfybz4iE1JCROi9fInKlzr5xABq8nb9Qc%2FJ9DIQM%2BXmk%3D`
req.Headers.Set("x-forwarded-tls-client-cert", testcertificate)
resp, err := client.RawRequest(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
buf := bytes.NewBuffer(nil)
buf.ReadFrom(resp.Body)
testcertificate, _ = url.QueryUnescape(testcertificate)
if !strings.Contains(buf.String(), strings.ReplaceAll(testcertificate, "\n", "")) {
t.Fatalf("bad body: %v vs %v", buf.String(), testcertificate)
}
})
}
59 changes: 59 additions & 0 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ package http
import (
"bytes"
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -507,6 +510,8 @@ func WrapForwardedForHandler(h http.Handler, l *configutil.Listener) http.Handle
hopSkips := l.XForwardedForHopSkips
authorizedAddrs := l.XForwardedForAuthorizedAddrs
rejectNotAuthz := l.XForwardedForRejectNotAuthorized
clientCertHeader := l.XForwardedForClientCertHeader
clientCertHeaderDecoders := l.XForwardedForClientCertHeaderDecoders
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
if !headersOK || len(headers) == 0 {
Expand Down Expand Up @@ -589,6 +594,60 @@ func WrapForwardedForHandler(h http.Handler, l *configutil.Listener) http.Handle
}

r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)

// Import the Client Certificate forwarded by the reverse proxy
// There should be only 1 instance of the header, but looping allows for more flexibility
clientCertHeaders, clientCertHeadersOK := r.Header[textproto.CanonicalMIMEHeaderKey(clientCertHeader)]
if clientCertHeadersOK && len(clientCertHeaders) > 0 {
var client_certs []*x509.Certificate
for _, header := range clientCertHeaders {
// Multiple certs should be comma delimetered
vals := strings.Split(header, ",")
for _, v := range vals {
actions := strings.Split(clientCertHeaderDecoders, ",")
for _, action := range actions {
switch action {
case "URL":
decoded, err := url.QueryUnescape(v)
if err != nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("failed to url unescape the client certificate: %w", err))
return
}
v = decoded
case "BASE64":
decoded, err := base64.StdEncoding.DecodeString(v)
if err != nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("failed to base64 decode the client certificate: %w", err))
return
}
v = string(decoded[:])
case "DER":
decoded, _ := pem.Decode([]byte(v))
if decoded == nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("failed to convert the client certificate to DER format: %w", err))
return
}
v = string(decoded.Bytes[:])
default:
respondError(w, http.StatusBadRequest, fmt.Errorf("unknown decode option specified: %s", action))
return
}
}

cert, err := x509.ParseCertificate([]byte(v))
if err != nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("failed to parse the client certificate: %w", err))
return
}
client_certs = append(client_certs, cert)
}
}
if r.TLS == nil {
respondError(w, http.StatusBadRequest, fmt.Errorf("Server must use TLS for certificate authentication"))
} else {
r.TLS.PeerCertificates = append(client_certs, r.TLS.PeerCertificates...)
}
}
h.ServeHTTP(w, r)
})
}
Expand Down
18 changes: 10 additions & 8 deletions internalshared/configutil/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,16 @@ type Listener struct {
ProxyProtocolAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"`
ProxyProtocolAuthorizedAddrsRaw interface{} `hcl:"proxy_protocol_authorized_addrs,alias:ProxyProtocolAuthorizedAddrs"`

XForwardedForAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"`
XForwardedForAuthorizedAddrsRaw interface{} `hcl:"x_forwarded_for_authorized_addrs,alias:XForwardedForAuthorizedAddrs"`
XForwardedForHopSkips int64 `hcl:"-"`
XForwardedForHopSkipsRaw interface{} `hcl:"x_forwarded_for_hop_skips,alias:XForwardedForHopSkips"`
XForwardedForRejectNotPresent bool `hcl:"-"`
XForwardedForRejectNotPresentRaw interface{} `hcl:"x_forwarded_for_reject_not_present,alias:XForwardedForRejectNotPresent"`
XForwardedForRejectNotAuthorized bool `hcl:"-"`
XForwardedForRejectNotAuthorizedRaw interface{} `hcl:"x_forwarded_for_reject_not_authorized,alias:XForwardedForRejectNotAuthorized"`
XForwardedForAuthorizedAddrs []*sockaddr.SockAddrMarshaler `hcl:"-"`
XForwardedForAuthorizedAddrsRaw interface{} `hcl:"x_forwarded_for_authorized_addrs,alias:XForwardedForAuthorizedAddrs"`
XForwardedForHopSkips int64 `hcl:"-"`
XForwardedForHopSkipsRaw interface{} `hcl:"x_forwarded_for_hop_skips,alias:XForwardedForHopSkips"`
XForwardedForRejectNotPresent bool `hcl:"-"`
XForwardedForRejectNotPresentRaw interface{} `hcl:"x_forwarded_for_reject_not_present,alias:XForwardedForRejectNotPresent"`
XForwardedForRejectNotAuthorized bool `hcl:"-"`
XForwardedForRejectNotAuthorizedRaw interface{} `hcl:"x_forwarded_for_reject_not_authorized,alias:XForwardedForRejectNotAuthorized"`
XForwardedForClientCertHeader string `hcl:"x_forwarded_for_client_cert_header,alias:XForwardedForClientCertHeader"`
XForwardedForClientCertHeaderDecoders string `hcl:"x_forwarded_for_client_cert_header_decoders,alias:XForwardedForClientCertHeaderDecoders"`

SocketMode string `hcl:"socket_mode"`
SocketUser string `hcl:"socket_user"`
Expand Down
20 changes: 20 additions & 0 deletions website/content/docs/configuration/listener/tcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ default value in the `"/sys/config/ui"` [API endpoint](/vault/api-docs/system/co
connecting client's IP, for example `3.4.5.6`. Note this requires the load balancer
to send the connecting client's IP in the `X-Forwarded-For` header.

- `x_forwarded_for_client_cert_header` `(string: "")` –
Specifies the header that will be used for the client certificate.
This is required if you use the [TLS Certificates Auth Method](/docs/auth/cert) and your
vault server is behind a reverse proxy.

- `x_forwarded_for_client_cert_header_decoders` `(string: "")` –
Comma delimited list that specifies the decoders that will be used to decode the client certificate.
This is required if you use the [TLS Certificates Auth Method](/docs/auth/cert) and your
vault server is behind a reverse proxy. The resulting certificate should be in DER format.
Available Values:

- BASE64 - Runs Base64 decode
- DER - Converts a pem certificate to der
- URL - Runs URL decode

Known Values:

- Traefik = "BASE64"
- NGINX = "URL,DER"

- `x_forwarded_for_hop_skips` `(string: "0")` – The number of addresses that will be
skipped from the _rear_ of the set of hops. For instance, for a header value
of `1.2.3.4, 2.3.4.5, 3.4.5.6, 4.5.6.7`, if this value is set to `"1"`, the address that
Expand Down
Loading