Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-pages.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaime Martinez <jmartinez@gitlab.com>2023-10-17 03:40:19 +0300
committerJaime Martinez <jmartinez@gitlab.com>2023-10-17 03:40:19 +0300
commit885055526d498a171120e69d7fe1c769814b73ec (patch)
tree3bb3960e6ffa8c2da47e9abf59c6c635aa49f170
parenta791ceb88fb6769fba5741a99419b6095899c86e (diff)
parent82a9981e93e32a2ae7528a247a7065d98240b924 (diff)
Merge branch 'mtls' into 'master'
Support for Mutual TLS See merge request https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/907 Merged-by: Jaime Martinez <jmartinez@gitlab.com> Approved-by: Jaime Martinez <jmartinez@gitlab.com> Approved-by: Costel Maxim <cmaxim@gitlab.com> Reviewed-by: Jaime Martinez <jmartinez@gitlab.com> Reviewed-by: Hayley Swimelar <hswimelar@gitlab.com> Co-authored-by: Django Cass <django@dcas.dev>
-rw-r--r--README.md32
-rw-r--r--app.go21
-rw-r--r--internal/config/config.go47
-rw-r--r--internal/config/config_test.go65
-rw-r--r--internal/config/flags.go4
-rw-r--r--internal/domain/domain.go29
-rw-r--r--internal/domain/domain_test.go8
-rw-r--r--internal/logging/logging.go14
-rw-r--r--internal/source/gitlab/api/virtual_domain.go5
-rw-r--r--internal/source/gitlab/gitlab.go2
-rw-r--r--internal/tls/testdata/cert.crt11
-rw-r--r--internal/tls/tls.go64
-rw-r--r--internal/tls/tls_test.go32
-rw-r--r--shared/pages/group.https-only/project6/public.zipbin0 -> 326 bytes
-rw-r--r--shared/pages/group.https-only/project6/public/index.html0
-rw-r--r--test/acceptance/acceptance_test.go19
-rw-r--r--test/acceptance/config_test.go2
-rw-r--r--test/acceptance/helpers_test.go55
-rw-r--r--test/acceptance/ratelimiter_test.go5
-rw-r--r--test/acceptance/testdata/ca.crt33
-rw-r--r--test/acceptance/testdata/ca.key52
-rw-r--r--test/acceptance/testdata/ca.srl1
-rw-r--r--test/acceptance/testdata/client.crt23
-rw-r--r--test/acceptance/testdata/client.csr10
-rw-r--r--test/acceptance/testdata/client.key5
-rw-r--r--test/acceptance/tls_test.go118
-rw-r--r--test/gitlabstub/api_responses.go35
-rw-r--r--test/gitlabstub/handlers.go2
28 files changed, 640 insertions, 54 deletions
diff --git a/README.md b/README.md
index 7a93ad05..d37b506c 100644
--- a/README.md
+++ b/README.md
@@ -154,6 +154,38 @@ $ ./gitlab-pages -listen-http "10.0.0.1:8080" -listen-https "[fd00::1]:8080" -pa
session cookie. This is done via a request to GitLab API with the user's access token.
6. If token is invalidated, user will be redirected again to GitLab to authorize pages again.
+### mTLS
+
+Pages can optionally server content using mutual TLS at the instance level.
+When enabled, clients will be required to present an `x509` certificate in order to view content.
+
+Example:
+
+```
+$ make
+$ ./gitlab-pages -listen-https ":9090" -root-cert=path/to/example.com.crt -root-key=path/to/example.com.key -pages-root path/to/gitlab/shared/pages -pages-domain example.com -tls-client-auth=RequireAndVerifyClientCert -tls-client-cert path/to/client/cert.crt
+```
+
+Pages can also require client authentication for a list of specific domains.
+When enabled client authentication will **only be active for the supplied domains**, all other domains will use normal Pages authentication methods.
+
+Example:
+
+```shell
+$ make
+$ ./gitlab-pages -listen-https ":9090" -root-cert=path/to/example.com.crt -root-key=path/to/example.com.key -pages-root path/to/gitlab/shared/pages -pages-domain example.com -tls-client-auth=RequireAndVerifyClientCert -tls-client-cert path/to/client/cert.crt -tls-client-auth-domains domain1.example.org,domain2.example.org
+```
+
+The available values for `tls-client-auth` are:
+
+| Value | Description |
+|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `noclientcert` | Indicates that no client certificate should be requested during the handshake, and if any certificates are sent they will not be verified. |
+| `requestclientcert` | Indicates that a client certificate should be requested during the handshake, but does not require that the client send any certificates. |
+| `requireanyclientcert` | Indicates that a client certificate should be requested during the handshake, and that at least one certificate is required to be sent by the client, but that certificate is not required to be valid. |
+| `verifyclientcertifgiven` | Indicates that a client certificate should be requested during the handshake, but does not require that the client sends a certificate. If the client does send a certificate it is required to be valid. |
+| `requireandverifyclientcert` | Indicates that a client certificate should be requested during the handshake, and that at least one valid certificate is required to be sent by the client. |
+
### Enable Prometheus Metrics
For monitoring purposes, you can pass the `-metrics-address` flag when starting.
diff --git a/app.go b/app.go
index de650a93..b022d7c2 100644
--- a/app.go
+++ b/app.go
@@ -58,6 +58,25 @@ type theApp struct {
Handlers *handlers.Handlers
}
+func (a *theApp) GetConfig(ch *cryptotls.ClientHelloInfo) (*cryptotls.Config, error) {
+ if ch.ServerName == "" {
+ return nil, nil
+ }
+
+ if domain, _ := a.source.GetDomain(ch.Context(), ch.ServerName); domain != nil {
+ certPool, _ := domain.EnsureClientCertPool()
+ if certPool != nil {
+ // set MinVersion to fix gosec: G402
+ tlsConfig := &cryptotls.Config{MinVersion: cryptotls.VersionTLS12}
+ tlsConfig.ClientCAs = certPool
+ tlsConfig.ClientAuth = cryptotls.RequireAndVerifyClientCert
+ return tlsConfig, nil
+ }
+ }
+
+ return nil, nil
+}
+
func (a *theApp) GetCertificate(ch *cryptotls.ClientHelloInfo) (*cryptotls.Certificate, error) {
if ch.ServerName == "" {
return nil, nil
@@ -80,7 +99,7 @@ func (a *theApp) getTLSConfig() (*cryptotls.Config, error) {
}
var err error
- a.tlsConfig, err = tls.GetTLSConfig(a.config, a.GetCertificate)
+ a.tlsConfig, err = tls.GetTLSConfig(a.config, a.GetCertificate, a.GetConfig)
return a.tlsConfig, err
}
diff --git a/internal/config/config.go b/internal/config/config.go
index afd7982e..d2016550 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -140,8 +140,11 @@ type Sentry struct {
// TLS groups settings related to configuring TLS
type TLS struct {
- MinVersion uint16
- MaxVersion uint16
+ MinVersion uint16
+ MaxVersion uint16
+ ClientAuth tls.ClientAuthType
+ ClientCert string
+ ClientAuthDomains []string
}
// ZipServing groups settings to be used by the zip VFS opening and caching
@@ -246,6 +249,31 @@ func loadMetricsConfig() (metrics Metrics, err error) {
return metrics, nil
}
+// parseClientAuthType converts the tls.ClientAuthType enum from names
+// to the underlying value. Passing an empty string assumes
+// tls.NoClientCert
+func parseClientAuthType(clientAuth string) (tls.ClientAuthType, error) {
+ switch strings.ToLower(clientAuth) {
+ // if nothing is provided, assume that
+ // the user does not want to enable any form
+ // of client authentication
+ case "":
+ fallthrough
+ case "noclientcert":
+ return tls.NoClientCert, nil
+ case "requestclientcert":
+ return tls.RequestClientCert, nil
+ case "requireanyclientcert":
+ return tls.RequireAnyClientCert, nil
+ case "verifyclientcertifgiven":
+ return tls.VerifyClientCertIfGiven, nil
+ case "requireandverifyclientcert":
+ return tls.RequireAndVerifyClientCert, nil
+ default:
+ return -1, fmt.Errorf("unknown client auth type %s: supported values can be found at https://pkg.go.dev/crypto/tls#ClientAuthType", clientAuth)
+ }
+}
+
func parseHeaderString(customHeaders []string) (http.Header, error) {
headers := make(http.Header, len(customHeaders))
@@ -341,8 +369,10 @@ func loadConfig() (*Config, error) {
Environment: *sentryEnvironment,
},
TLS: TLS{
- MinVersion: allTLSVersions[*tlsMinVersion],
- MaxVersion: allTLSVersions[*tlsMaxVersion],
+ MinVersion: allTLSVersions[*tlsMinVersion],
+ MaxVersion: allTLSVersions[*tlsMaxVersion],
+ ClientCert: *tlsClientCert,
+ ClientAuthDomains: tlsClientAuthDomains.value,
},
Zip: ZipServing{
ExpirationInterval: *zipCacheExpiration,
@@ -394,6 +424,12 @@ func loadConfig() (*Config, error) {
return nil, fmt.Errorf("unable to parse header string: %w", err)
}
+ clientAuthType, err := parseClientAuthType(*tlsClientAuth)
+ if err != nil {
+ return nil, err
+ }
+ config.TLS.ClientAuth = clientAuthType
+
config.General.CustomHeaders = customHeaders
// Populating remaining GitLab settings
@@ -437,6 +473,9 @@ func logFields(config *Config) map[string]any {
"status_path": config.General.StatusPath,
"tls-min-version": *tlsMinVersion,
"tls-max-version": *tlsMaxVersion,
+ "tls-client-auth": *tlsClientAuth,
+ "tls-client-cert": *tlsClientCert,
+ "tls-client-auth-domains": tlsClientAuthDomains,
"gitlab-server": config.GitLab.PublicServer,
"internal-gitlab-server": config.GitLab.InternalServer,
"api-secret-key": *gitLabAPISecretKey,
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 9db88acc..516d7472 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,6 +1,7 @@
package config
import (
+ "crypto/tls"
"os"
"path/filepath"
"testing"
@@ -107,6 +108,70 @@ func setupHTTPSFixture(t *testing.T) (dir string, key string, cert string) {
return tmpDir, keyfile.Name(), certfile.Name()
}
+func TestParseClientAuthType(t *testing.T) {
+ tests := []struct {
+ name string
+ clientAuth string
+ valid bool
+ expected tls.ClientAuthType
+ }{
+ {
+ name: "empty string",
+ clientAuth: "",
+ valid: true,
+ expected: tls.NoClientCert,
+ },
+ {
+ name: "unknown value",
+ clientAuth: "no cert",
+ valid: false,
+ expected: -1,
+ },
+ {
+ name: "explicitly no cert",
+ clientAuth: "NoClientCert",
+ valid: true,
+ expected: tls.NoClientCert,
+ },
+ {
+ name: "request cert",
+ clientAuth: "RequestClientCert",
+ valid: true,
+ expected: tls.RequestClientCert,
+ },
+ {
+ name: "require any cert",
+ clientAuth: "RequireAnyClientCert",
+ valid: true,
+ expected: tls.RequireAnyClientCert,
+ },
+ {
+ name: "verify cert if given",
+ clientAuth: "VerifyClientCertIfGiven",
+ valid: true,
+ expected: tls.VerifyClientCertIfGiven,
+ },
+ {
+ name: "require and verify cert",
+ clientAuth: "RequireAndVerifyClientCert",
+ valid: true,
+ expected: tls.RequireAndVerifyClientCert,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ authType, err := parseClientAuthType(tt.clientAuth)
+ if tt.valid {
+ require.NoError(t, err)
+ require.EqualValues(t, tt.expected, authType)
+ return
+ }
+ require.Error(t, err)
+ })
+ }
+}
+
func TestParseHeaderString(t *testing.T) {
tests := []struct {
name string
diff --git a/internal/config/flags.go b/internal/config/flags.go
index c05f2f32..bce3b07e 100644
--- a/internal/config/flags.go
+++ b/internal/config/flags.go
@@ -80,6 +80,9 @@ var (
insecureCiphers = flag.Bool("insecure-ciphers", false, "Use default list of cipher suites, may contain insecure ones like 3DES and RC4")
tlsMinVersion = flag.String("tls-min-version", "tls1.2", tlsVersionFlagUsage("min"))
tlsMaxVersion = flag.String("tls-max-version", "", tlsVersionFlagUsage("max"))
+ tlsClientAuth = flag.String("tls-client-auth", "noclientcert", "Determines the TLS servers policy for client authentication. Defaults to no client certificate. Values can be found at https://pkg.go.dev/crypto/tls#ClientAuthType")
+ tlsClientCert = flag.String("tls-client-cert", "", "Path to the certificate authority used to validate client certificates against")
+ tlsClientAuthDomains = MultiStringFlag{separator: ","}
zipCacheExpiration = flag.Duration("zip-cache-expiration", 60*time.Second, "Zip serving archive cache expiration interval")
zipCacheCleanup = flag.Duration("zip-cache-cleanup", 30*time.Second, "Zip serving archive cache cleanup interval")
zipCacheRefresh = flag.Duration("zip-cache-refresh", 30*time.Second, "Zip serving archive cache refresh interval")
@@ -120,6 +123,7 @@ func initFlags() {
flag.Var(&listenProxy, "listen-proxy", "The address(es) or unix socket paths to listen on for proxy requests")
flag.Var(&listenHTTPSProxyv2, "listen-https-proxyv2", "The address(es) or unix socket paths to listen on for HTTPS PROXYv2 requests (https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)")
flag.Var(&header, "header", "The additional http header(s) that should be send to the client")
+ flag.Var(&tlsClientAuthDomains, "tls-client-auth-domains", "The domain(s) that require client certificate authentication")
// read from -config=/path/to/gitlab-pages-config
flag.String(flag.DefaultConfigFlagname, "", "path to config file")
diff --git a/internal/domain/domain.go b/internal/domain/domain.go
index 1cfee100..9623ce51 100644
--- a/internal/domain/domain.go
+++ b/internal/domain/domain.go
@@ -3,6 +3,7 @@ package domain
import (
"context"
"crypto/tls"
+ "crypto/x509"
"errors"
"net/http"
"sync"
@@ -19,9 +20,10 @@ var ErrDomainDoesNotExist = errors.New("domain does not exist")
// Domain is a domain that gitlab-pages can serve.
type Domain struct {
- Name string
- CertificateCert string
- CertificateKey string
+ Name string
+ CertificateCert string
+ CertificateKey string
+ ClientCertificateCert string
Resolver Resolver
@@ -31,12 +33,13 @@ type Domain struct {
}
// New creates a new domain with a resolver and existing certificates
-func New(name, cert, key string, resolver Resolver) *Domain {
+func New(name, cert, key, clientCert string, resolver Resolver) *Domain {
return &Domain{
- Name: name,
- CertificateCert: cert,
- CertificateKey: key,
- Resolver: resolver,
+ Name: name,
+ CertificateCert: cert,
+ CertificateKey: key,
+ ClientCertificateCert: clientCert,
+ Resolver: resolver,
}
}
@@ -121,6 +124,16 @@ func (d *Domain) EnsureCertificate() (*tls.Certificate, error) {
return d.certificate, d.certificateError
}
+func (d *Domain) EnsureClientCertPool() (*x509.CertPool, error) {
+ if d == nil || len(d.ClientCertificateCert) == 0 {
+ return nil, errors.New("tls client certificates can be loaded only for pages with configuration")
+ }
+
+ certPool := x509.NewCertPool()
+ certPool.AppendCertsFromPEM([]byte(d.ClientCertificateCert))
+ return certPool, nil
+}
+
// ServeFileHTTP returns true if something was served, false if not.
func (d *Domain) ServeFileHTTP(w http.ResponseWriter, r *http.Request) bool {
request, err := d.resolve(r)
diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go
index 5698aead..66a3a043 100644
--- a/internal/domain/domain_test.go
+++ b/internal/domain/domain_test.go
@@ -33,7 +33,7 @@ func TestIsHTTPSOnly(t *testing.T) {
}{
{
name: "Custom domain with HTTPS-only enabled",
- domain: domain.New("custom-domain", "", "",
+ domain: domain.New("custom-domain", "", "", "",
mockResolver(t,
&serving.LookupPath{
Path: "group/project/public",
@@ -48,7 +48,7 @@ func TestIsHTTPSOnly(t *testing.T) {
},
{
name: "Custom domain with HTTPS-only disabled",
- domain: domain.New("custom-domain", "", "",
+ domain: domain.New("custom-domain", "", "", "",
mockResolver(t,
&serving.LookupPath{
Path: "group/project/public",
@@ -63,7 +63,7 @@ func TestIsHTTPSOnly(t *testing.T) {
},
{
name: "Unknown project",
- domain: domain.New("", "", "", mockResolver(t, nil, "", domain.ErrDomainDoesNotExist)),
+ domain: domain.New("", "", "", "", mockResolver(t, nil, "", domain.ErrDomainDoesNotExist)),
url: "http://test-domain/project",
expected: false,
},
@@ -81,7 +81,7 @@ func TestPredefined404ServeHTTP(t *testing.T) {
cleanup := setUpTests(t)
defer cleanup()
- testDomain := domain.New("", "", "", mockResolver(t, nil, "", domain.ErrDomainDoesNotExist))
+ testDomain := domain.New("", "", "", "", mockResolver(t, nil, "", domain.ErrDomainDoesNotExist))
require.HTTPStatusCode(t, serveFileOrNotFound(testDomain), http.MethodGet, "http://group.test.io/not-existing-file", nil, http.StatusNotFound)
require.HTTPBodyContains(t, serveFileOrNotFound(testDomain), http.MethodGet, "http://group.test.io/not-existing-file", nil, "The page you're looking for could not be found")
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
index 2faaddc5..47083d8e 100644
--- a/internal/logging/logging.go
+++ b/internal/logging/logging.go
@@ -1,6 +1,7 @@
package logging
import (
+ "fmt"
"net/http"
"github.com/sirupsen/logrus"
@@ -66,9 +67,20 @@ func BasicAccessLogger(handler http.Handler, format string) (http.Handler, error
}
func extraFields(r *http.Request) log.Fields {
- return log.Fields{
+ fields := log.Fields{
"pages_https": request.IsHTTPS(r),
}
+ // if there's no client cert, return early
+ if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
+ return fields
+ }
+
+ // log the client certificate information
+ for i := range r.TLS.PeerCertificates {
+ fields[fmt.Sprintf("x509_subject_%d", i)] = r.TLS.PeerCertificates[i].Subject.ToRDNSequence().String()
+ fields[fmt.Sprintf("x509_issuer_%d", i)] = r.TLS.PeerCertificates[i].Issuer.ToRDNSequence().String()
+ }
+ return fields
}
// LogRequest will inject request host and path to the logged messages
diff --git a/internal/source/gitlab/api/virtual_domain.go b/internal/source/gitlab/api/virtual_domain.go
index 200c06de..ba55db04 100644
--- a/internal/source/gitlab/api/virtual_domain.go
+++ b/internal/source/gitlab/api/virtual_domain.go
@@ -3,8 +3,9 @@ package api
// VirtualDomain represents a GitLab Pages virtual domain that is being sent
// from GitLab API
type VirtualDomain struct {
- Certificate string `json:"certificate,omitempty"`
- Key string `json:"key,omitempty"`
+ Certificate string `json:"certificate,omitempty"`
+ Key string `json:"key,omitempty"`
+ ClientCertificate string `json:"client_certificate,omitempty"`
LookupPaths []LookupPath `json:"lookup_paths"`
}
diff --git a/internal/source/gitlab/gitlab.go b/internal/source/gitlab/gitlab.go
index ab5bc490..d3fbc8cc 100644
--- a/internal/source/gitlab/gitlab.go
+++ b/internal/source/gitlab/gitlab.go
@@ -56,7 +56,7 @@ func (g *Gitlab) GetDomain(ctx context.Context, name string) (*domain.Domain, er
// TODO introduce a second-level cache for domains, invalidate using etags
// from first-level cache
- d := domain.New(name, lookup.Domain.Certificate, lookup.Domain.Key, g)
+ d := domain.New(name, lookup.Domain.Certificate, lookup.Domain.Key, lookup.Domain.ClientCertificate, g)
return d, nil
}
diff --git a/internal/tls/testdata/cert.crt b/internal/tls/testdata/cert.crt
new file mode 100644
index 00000000..4952c7ed
--- /dev/null
+++ b/internal/tls/testdata/cert.crt
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
+DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
+EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
+7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
+5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
+BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
+NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
+Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
+6MF9+Yw1Yy0t
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/internal/tls/tls.go b/internal/tls/tls.go
index eb8e4e64..a01cceb8 100644
--- a/internal/tls/tls.go
+++ b/internal/tls/tls.go
@@ -2,6 +2,8 @@ package tls
import (
"crypto/tls"
+ "crypto/x509"
+ "os"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
"gitlab.com/gitlab-org/gitlab-pages/internal/ratelimiter"
@@ -23,9 +25,13 @@ var preferredCipherSuites = []uint16{
// GetCertificateFunc returns the certificate to be used for given domain
type GetCertificateFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error)
+// GetConfigFunc returns a tls.Config with populated client
+// auth values.
+type GetConfigFunc func(*tls.ClientHelloInfo) (*tls.Config, error)
+
// GetTLSConfig initializes tls.Config based on config flags
// getCertificateByServerName obtains certificate based on domain
-func GetTLSConfig(cfg *config.Config, getCertificateByServerName GetCertificateFunc) (*tls.Config, error) {
+func GetTLSConfig(cfg *config.Config, getCertificateByServerName GetCertificateFunc, getConfigByServerName GetConfigFunc) (*tls.Config, error) {
wildcardCertificate, err := tls.X509KeyPair(cfg.General.RootCertificate, cfg.General.RootKey)
if err != nil {
return nil, err
@@ -74,8 +80,48 @@ func GetTLSConfig(cfg *config.Config, getCertificateByServerName GetCertificateF
getCertificate = TLSDomainRateLimiter.GetCertificateMiddleware(getCertificate)
getCertificate = TLSSourceIPRateLimiter.GetCertificateMiddleware(getCertificate)
+ tlsConfig, err := getTLSConfig(cfg, getCertificate)
+ if err != nil {
+ return nil, err
+ }
+
+ tlsConfig.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
+ return getOptionalConfig(cfg, info, getCertificate, getConfigByServerName)
+ }
+
+ return tlsConfig, nil
+}
+
+func getOptionalConfig(cfg *config.Config, info *tls.ClientHelloInfo, getCertificate GetCertificateFunc, getConfigByServerName GetConfigFunc) (*tls.Config, error) {
+ customConfig, err := getConfigByServerName(info)
+
+ if customConfig != nil || err != nil {
+ customConfig.GetCertificate = getCertificate
+ return customConfig, err
+ }
+
+ if cfg.TLS.ClientAuth == tls.NoClientCert {
+ return nil, nil
+ }
+
+ for _, i := range cfg.TLS.ClientAuthDomains {
+ if i != info.ServerName {
+ continue
+ }
+ tlsConfig, err := getTLSConfig(cfg, getCertificate)
+ if err != nil {
+ return nil, err
+ }
+ tlsConfig.ClientAuth = cfg.TLS.ClientAuth
+ return tlsConfig, nil
+ }
+
+ return nil, nil
+}
+
+func getTLSConfig(cfg *config.Config, getCertificateByServerName GetCertificateFunc) (*tls.Config, error) {
// set MinVersion to fix gosec: G402
- tlsConfig := &tls.Config{GetCertificate: getCertificate, MinVersion: tls.VersionTLS12}
+ tlsConfig := &tls.Config{GetCertificate: getCertificateByServerName, MinVersion: tls.VersionTLS12}
if !cfg.General.InsecureCiphers {
tlsConfig.CipherSuites = preferredCipherSuites
@@ -84,5 +130,19 @@ func GetTLSConfig(cfg *config.Config, getCertificateByServerName GetCertificateF
tlsConfig.MinVersion = cfg.TLS.MinVersion
tlsConfig.MaxVersion = cfg.TLS.MaxVersion
+ if len(cfg.TLS.ClientAuthDomains) == 0 {
+ tlsConfig.ClientAuth = cfg.TLS.ClientAuth
+ }
+
+ if cfg.TLS.ClientAuth > tls.RequestClientCert {
+ caCert, err := os.ReadFile(cfg.TLS.ClientCert)
+ if err != nil {
+ return nil, err
+ }
+ certPool := x509.NewCertPool()
+ certPool.AppendCertsFromPEM(caCert)
+ tlsConfig.ClientCAs = certPool
+ }
+
return tlsConfig, nil
}
diff --git a/internal/tls/tls_test.go b/internal/tls/tls_test.go
index 012d335b..358cc72a 100644
--- a/internal/tls/tls_test.go
+++ b/internal/tls/tls_test.go
@@ -31,9 +31,13 @@ var getCertificate = func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, nil
}
+var getConfig = func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
+ return &tls.Config{MinVersion: tls.VersionTLS12}, nil
+}
+
func TestInvalidKeyPair(t *testing.T) {
cfg := &config.Config{}
- _, err := GetTLSConfig(cfg, getCertificate)
+ _, err := GetTLSConfig(cfg, getCertificate, getConfig)
require.EqualError(t, err, "tls: failed to find any PEM data in certificate input")
}
@@ -45,11 +49,30 @@ func TestInsecureCiphers(t *testing.T) {
InsecureCiphers: true,
},
}
- tlsConfig, err := GetTLSConfig(cfg, getCertificate)
+ tlsConfig, err := GetTLSConfig(cfg, getCertificate, getConfig)
require.NoError(t, err)
require.Empty(t, tlsConfig.CipherSuites)
}
+func TestClientCert(t *testing.T) {
+ cfg := &config.Config{
+ General: config.General{
+ RootCertificate: cert,
+ RootKey: key,
+ },
+ TLS: config.TLS{
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ ClientCert: "./testdata/cert.crt",
+ },
+ }
+ tlsConfig, err := GetTLSConfig(cfg, getCertificate, getConfig)
+ require.NoError(t, err)
+ require.IsType(t, getCertificate, tlsConfig.GetCertificate)
+ require.IsType(t, getConfig, tlsConfig.GetConfigForClient)
+ require.Equal(t, tls.RequireAndVerifyClientCert, tlsConfig.ClientAuth)
+ require.NotNil(t, tlsConfig.ClientCAs)
+}
+
func TestGetTLSConfig(t *testing.T) {
cfg := &config.Config{
General: config.General{
@@ -61,12 +84,15 @@ func TestGetTLSConfig(t *testing.T) {
MaxVersion: tls.VersionTLS12,
},
}
- tlsConfig, err := GetTLSConfig(cfg, getCertificate)
+ tlsConfig, err := GetTLSConfig(cfg, getCertificate, getConfig)
require.NoError(t, err)
require.IsType(t, getCertificate, tlsConfig.GetCertificate)
+ require.IsType(t, getConfig, tlsConfig.GetConfigForClient)
require.Equal(t, preferredCipherSuites, tlsConfig.CipherSuites)
require.Equal(t, uint16(tls.VersionTLS11), tlsConfig.MinVersion)
require.Equal(t, uint16(tls.VersionTLS12), tlsConfig.MaxVersion)
+ require.Equal(t, tls.NoClientCert, tlsConfig.ClientAuth)
+ require.Nil(t, tlsConfig.ClientCAs)
cert, err := tlsConfig.GetCertificate(&tls.ClientHelloInfo{})
require.NoError(t, err)
diff --git a/shared/pages/group.https-only/project6/public.zip b/shared/pages/group.https-only/project6/public.zip
new file mode 100644
index 00000000..ead779f5
--- /dev/null
+++ b/shared/pages/group.https-only/project6/public.zip
Binary files differ
diff --git a/shared/pages/group.https-only/project6/public/index.html b/shared/pages/group.https-only/project6/public/index.html
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/shared/pages/group.https-only/project6/public/index.html
diff --git a/test/acceptance/acceptance_test.go b/test/acceptance/acceptance_test.go
index 3b41eaee..2ac06a42 100644
--- a/test/acceptance/acceptance_test.go
+++ b/test/acceptance/acceptance_test.go
@@ -23,19 +23,21 @@ var (
httpsPort = "37000"
httpProxyPort = "38000"
httpProxyV2Port = "39000"
+ httpsCertPort = "40000"
// TODO: Use TCP port 0 everywhere to avoid conflicts. The binary could output
// the actual port (and type of listener) for us to read in place of the
// hardcoded values below.
listeners = []ListenSpec{
- {"http", "127.0.0.1", httpPort},
- {"http", "::1", httpPort},
- {"https", "127.0.0.1", httpsPort},
- {"https", "::1", httpsPort},
- {"proxy", "127.0.0.1", httpProxyPort},
- {"proxy", "::1", httpProxyPort},
- {"https-proxyv2", "127.0.0.1", httpProxyV2Port},
- {"https-proxyv2", "::1", httpProxyV2Port},
+ {"http", "127.0.0.1", httpPort, false},
+ {"http", "::1", httpPort, false},
+ {"https", "127.0.0.1", httpsPort, false},
+ {"https", "::1", httpsPort, false},
+ {"proxy", "127.0.0.1", httpProxyPort, false},
+ {"proxy", "::1", httpProxyPort, false},
+ {"https-proxyv2", "127.0.0.1", httpProxyV2Port, false},
+ {"https-proxyv2", "::1", httpProxyV2Port, false},
+ {"https", "127.0.0.1", httpsCertPort, true},
}
ipv4Listeners = []ListenSpec{
@@ -49,6 +51,7 @@ var (
httpsListener = listeners[2]
proxyListener = listeners[4]
httpsProxyv2Listener = listeners[6]
+ clientCertListener = listeners[8]
)
func TestMain(m *testing.M) {
diff --git a/test/acceptance/config_test.go b/test/acceptance/config_test.go
index baa35f6e..53f0d174 100644
--- a/test/acceptance/config_test.go
+++ b/test/acceptance/config_test.go
@@ -45,7 +45,7 @@ func TestMixedConfigSources(t *testing.T) {
}
func TestMultipleListenersFromEnvironmentVariables(t *testing.T) {
- listenSpecs := []ListenSpec{{"http", "127.0.0.1", "37001"}, {"http", "127.0.0.1", "37002"}}
+ listenSpecs := []ListenSpec{{"http", "127.0.0.1", "37001", false}, {"http", "127.0.0.1", "37002", false}}
t.Setenv("LISTEN_HTTP", fmt.Sprintf("%s,%s", net.JoinHostPort("127.0.0.1", "37001"), net.JoinHostPort("127.0.0.1", "37002")))
RunPagesProcess(t,
diff --git a/test/acceptance/helpers_test.go b/test/acceptance/helpers_test.go
index f8638e36..d089c091 100644
--- a/test/acceptance/helpers_test.go
+++ b/test/acceptance/helpers_test.go
@@ -78,9 +78,10 @@ func (b *LogCaptureBuffer) Reset() {
// ListenSpec is used to point at a gitlab-pages http server, preserving the
// type of port it is (http, https, proxy)
type ListenSpec struct {
- Type string
- Host string
- Port string
+ Type string
+ Host string
+ Port string
+ ClientCert bool
}
func supportedListeners() []ListenSpec {
@@ -166,25 +167,45 @@ func (l ListenSpec) dialContext() dialContext {
return l.httpsDialContext()
}
-func (l ListenSpec) Client() *http.Client {
+func (l ListenSpec) Client(host string) (*http.Client, error) {
+ var certificates []tls.Certificate
+ if l.ClientCert {
+ clientCert, err := tls.X509KeyPair([]byte(fixtureClientCert), []byte(fixtureClientKey))
+ if err != nil {
+ return nil, err
+ }
+ certificates = []tls.Certificate{clientCert}
+ }
return &http.Client{
Transport: &http.Transport{
- TLSClientConfig: &tls.Config{RootCAs: TestCertPool},
+ TLSClientConfig: &tls.Config{
+ RootCAs: TestCertPool,
+ Certificates: certificates,
+ ServerName: host, // forcibly set the SNI
+ InsecureSkipVerify: true,
+ },
DialContext: l.dialContext(),
ResponseHeaderTimeout: 5 * time.Second,
},
- }
+ }, nil
}
// Use a very short timeout to repeatedly check for the server to be up.
-func (l ListenSpec) QuickTimeoutClient() *http.Client {
+func (l ListenSpec) QuickTimeoutClient() (*http.Client, error) {
+ clientCert, err := tls.X509KeyPair([]byte(fixtureClientCert), []byte(fixtureClientKey))
+ if err != nil {
+ return nil, err
+ }
return &http.Client{
Transport: &http.Transport{
- TLSClientConfig: &tls.Config{RootCAs: TestCertPool},
+ TLSClientConfig: &tls.Config{
+ RootCAs: TestCertPool,
+ Certificates: []tls.Certificate{clientCert},
+ },
DialContext: l.dialContext(),
ResponseHeaderTimeout: 100 * time.Millisecond,
},
- }
+ }, nil
}
// Returns only once this spec points at a working TCP server
@@ -209,7 +230,11 @@ func (l ListenSpec) WaitUntilRequestSucceeds(done chan struct{}) error {
return err
}
- response, err := l.QuickTimeoutClient().Transport.RoundTrip(req)
+ client, err := l.QuickTimeoutClient()
+ if err != nil {
+ return err
+ }
+ response, err := client.Transport.RoundTrip(req)
if err == nil {
response.Body.Close()
@@ -425,7 +450,10 @@ func GetPageFromListenerWithHeaders(t *testing.T, spec ListenSpec, host, urlSuff
func DoPagesRequest(t *testing.T, spec ListenSpec, req *http.Request) (*http.Response, error) {
t.Logf("curl -X %s -H'Host: %s' %s", req.Method, req.Host, req.URL)
- return spec.Client().Do(req)
+ client, err := spec.Client(req.Host)
+ require.NoError(t, err)
+
+ return client.Do(req)
}
func GetRedirectPage(t *testing.T, spec ListenSpec, host, urlsuffix string) (*http.Response, error) {
@@ -460,7 +488,10 @@ func GetRedirectPageWithHeaders(t *testing.T, spec ListenSpec, host, urlsuffix s
req.Host = host
- return spec.Client().Transport.RoundTrip(req)
+ client, err := spec.Client(req.Host)
+ require.NoError(t, err)
+
+ return client.Transport.RoundTrip(req)
}
func ClientWithConfig(tlsConfig *tls.Config) (*http.Client, func()) {
diff --git a/test/acceptance/ratelimiter_test.go b/test/acceptance/ratelimiter_test.go
index 6f0fd656..f00be188 100644
--- a/test/acceptance/ratelimiter_test.go
+++ b/test/acceptance/ratelimiter_test.go
@@ -184,7 +184,10 @@ func makeTLSRequest(t *testing.T, spec ListenSpec) (*http.Response, error) {
req, err := http.NewRequest("GET", "https://group.gitlab-example.com/project", nil)
require.NoError(t, err)
- return spec.Client().Do(req)
+ client, err := spec.Client(req.Host)
+ require.NoError(t, err)
+
+ return client.Do(req)
}
func assertLogFound(t *testing.T, logBuf *LogCaptureBuffer, expectedLogs []string) {
diff --git a/test/acceptance/testdata/ca.crt b/test/acceptance/testdata/ca.crt
new file mode 100644
index 00000000..5ddf7b1e
--- /dev/null
+++ b/test/acceptance/testdata/ca.crt
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/test/acceptance/testdata/ca.key b/test/acceptance/testdata/ca.key
new file mode 100644
index 00000000..9dff438e
--- /dev/null
+++ b/test/acceptance/testdata/ca.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+kCr/frdUlJBUVxvzI82YqX6+ODg0QTQ=
+-----END PRIVATE KEY-----
diff --git a/test/acceptance/testdata/ca.srl b/test/acceptance/testdata/ca.srl
new file mode 100644
index 00000000..3e5f111c
--- /dev/null
+++ b/test/acceptance/testdata/ca.srl
@@ -0,0 +1 @@
+1C8504753745A008805BEDACDBE11CC860A86094
diff --git a/test/acceptance/testdata/client.crt b/test/acceptance/testdata/client.crt
new file mode 100644
index 00000000..069624ff
--- /dev/null
+++ b/test/acceptance/testdata/client.crt
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxDCCAawCFByFBHU3RaAIgFvtrNvhHMhgqGCUMA0GCSqGSIb3DQEBCwUAMHIx
+CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0MRUwEwYDVQQHDAxEZWZhdWx0IENp
+dHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxDTALBgNVBAsMBFRlc3Qx
+EDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMjMwOTIwMTEzNDM3WhcNMzMwOTE3MTEzNDM3
+WjCBlTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxFTATBgNVBAcMDERlZmF1
+bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDENMAsGA1UECwwE
+VGVzdDESMBAGA1UEAwwJVGVzdCBVc2VyMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4
+YW1wbGUub3JnMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5S6e7bNszpyhc279
+oRgW8TMytl0IGaPIhMoDF4oUXO+/sOK/cCz59II7nkwkk6COrvbh70ft8e9cdsqt
+a5UsvzANBgkqhkiG9w0BAQsFAAOCAgEAFGDKm6825SRYPTi7HG6uK1S1CXbPNnSy
+hv9umTa1iRLqip9eacNx6uijdRQr2IOOBZfuRAjut9IYlH/PfrNYPVcc/uYH/gIP
+LZdVjMxqpNxqQYUup3Wq8mVu8Gnfbm2ymvCPHnUhM4xH++grmUQW5bq7BozL/oea
+AfflJeS3AGmqPBNBDvjubn7NCirzKEkrt7COAXnpXmFmiErT9gWkeE1Xtn51/W3b
+27lu+5aBzDUtlppa5eT1GPc6fu5+HLtHod+nDn/7Ys/HUWw/5iA6DRhGXnLlz2W7
+C+m1yDtSVH+axhAXbhnzvZUpB6jXLLce99a5ff84fp/nJ96hR5lHekIkfNj0wtrQ
+WqvQ9HuU1Q3dbWOpC9xtOvQBINAnsZGrM8XwLzG39WdK2G9mLNQqHqKedCWW9UhE
+lsOZrYihuXOmTBCIW+SGDw2z4unsFdNMXXwJVv8REDJzGmq5JXai40nnnemS7JNQ
+ztgO1gPnznZQFsbHgvSSJOoi5sz/o2VeJJ+zYzPSc7Hzbm/MPlcOdXo2uLO6JrIN
+3Kk9Gdv/6tQ+gVUq3qNS/1cmiVTyNiiSOpLM1qLPVL/n5HFYPTbI3OQxRomkMb5j
+ab5Oj4+trDQQ1ysm6i8UEqQoCr/izj+2muIGp7u94UrcRmJkULwpjkRsQD/uX5yd
+hNo/s+Sx0hA=
+-----END CERTIFICATE-----
diff --git a/test/acceptance/testdata/client.csr b/test/acceptance/testdata/client.csr
new file mode 100644
index 00000000..ee5747aa
--- /dev/null
+++ b/test/acceptance/testdata/client.csr
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBUTCB+AIBADCBlTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxFTATBgNV
+BAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDEN
+MAsGA1UECwwEVGVzdDESMBAGA1UEAwwJVGVzdCBVc2VyMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUub3JnMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5S6e
+7bNszpyhc279oRgW8TMytl0IGaPIhMoDF4oUXO+/sOK/cCz59II7nkwkk6COrvbh
+70ft8e9cdsqta5Usv6AAMAoGCCqGSM49BAMCA0gAMEUCIQCPmp9LvyfueZTZSzw1
+kuzYJO2bCZFF5ScIlBPdDsxGyQIgafX/YBcLxqc70eSd88B/xxJPxG2bPCFERBi6
+fKb1b7g=
+-----END CERTIFICATE REQUEST-----
diff --git a/test/acceptance/testdata/client.key b/test/acceptance/testdata/client.key
new file mode 100644
index 00000000..17a6bff8
--- /dev/null
+++ b/test/acceptance/testdata/client.key
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIFWzobEUkcIgOUR81lz2KSmoPKyNI5UraogKlWOESRbHoAoGCCqGSM49
+AwEHoUQDQgAE5S6e7bNszpyhc279oRgW8TMytl0IGaPIhMoDF4oUXO+/sOK/cCz5
+9II7nkwkk6COrvbh70ft8e9cdsqta5Usvw==
+-----END EC PRIVATE KEY-----
diff --git a/test/acceptance/tls_test.go b/test/acceptance/tls_test.go
index 3b4c3a5c..f77bd20a 100644
--- a/test/acceptance/tls_test.go
+++ b/test/acceptance/tls_test.go
@@ -2,11 +2,129 @@ package acceptance_test
import (
"crypto/tls"
+ _ "embed"
+ "net/http"
"testing"
"github.com/stretchr/testify/require"
)
+//go:embed testdata/client.crt
+var fixtureClientCert string
+
+//go:embed testdata/client.key
+var fixtureClientKey string
+
+func TestGlobalMutualTLS(t *testing.T) {
+ RunPagesProcess(t,
+ withListeners([]ListenSpec{httpsListener}),
+ withExtraArgument("tls-client-auth", "requireandverifyclientcert"),
+ withExtraArgument("tls-client-cert", "../../test/acceptance/testdata/ca.crt"),
+ )
+
+ t.Run("client with cert works", func(t *testing.T) {
+ clientCert, err := tls.X509KeyPair([]byte(fixtureClientCert), []byte(fixtureClientKey))
+ require.NoError(t, err)
+
+ client, cleanup := ClientWithConfig(&tls.Config{
+ Certificates: []tls.Certificate{clientCert},
+ })
+ defer cleanup()
+
+ rsp, err := client.Get(httpsListener.URL("/"))
+ require.NoError(t, err)
+ require.NoError(t, rsp.Body.Close())
+ })
+ t.Run("client without cert fails", func(t *testing.T) {
+ client, cleanup := ClientWithConfig(&tls.Config{})
+ defer cleanup()
+
+ rsp, err := client.Get(httpsListener.URL("/"))
+ require.Error(t, err)
+ // TODO: Go versions >= 1.21 return "tls: certificate required"
+ require.ErrorContains(t, err, "tls: bad certificate")
+ require.Nil(t, rsp)
+ })
+}
+
+func TestGlobalDomainBasedMutualTLS(t *testing.T) {
+ RunPagesProcess(t,
+ withListeners([]ListenSpec{clientCertListener}),
+ withExtraArgument("tls-client-auth", "requireandverifyclientcert"),
+ withExtraArgument("tls-client-cert", "../../test/acceptance/testdata/ca.crt"),
+ withExtraArgument("tls-client-auth-domains", "test.gitlab-example.com"),
+ )
+
+ noClientCertListener := ListenSpec{
+ Type: httpsListener.Type,
+ Host: httpsListener.Host,
+ Port: clientCertListener.Port,
+ ClientCert: false,
+ }
+
+ t.Run("client with cert works to mtls domain", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, clientCertListener, "test.gitlab-example.com", "/")
+
+ require.NoError(t, err)
+ require.NoError(t, rsp.Body.Close())
+ })
+ t.Run("client without cert fails", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, noClientCertListener, "test.gitlab-example.com", "/")
+ require.Error(t, err)
+ // TODO: Go versions >= 1.21 return "tls: certificate required"
+ require.ErrorContains(t, err, "tls: bad certificate")
+ require.Nil(t, rsp)
+ })
+ t.Run("client without cert works to non-mtls domain", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, noClientCertListener, "other-test.gitlab-example.com", "/")
+
+ require.NoError(t, err)
+ require.NoError(t, rsp.Body.Close())
+ })
+}
+
+func TestGitLabAPIBasedMutualTLS(t *testing.T) {
+ RunPagesProcess(t,
+ withListeners([]ListenSpec{clientCertListener}),
+ )
+
+ noClientCertListener := ListenSpec{
+ Type: httpsListener.Type,
+ Host: httpsListener.Host,
+ Port: clientCertListener.Port,
+ ClientCert: false,
+ }
+
+ t.Run("client with cert works to mtls domain", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, clientCertListener, "mtls.gitlab-example.com", "/")
+
+ require.NoError(t, err)
+ require.EqualValues(t, http.StatusOK, rsp.StatusCode)
+ require.NoError(t, rsp.Body.Close())
+ })
+ t.Run("client without cert fails", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, noClientCertListener, "mtls.gitlab-example.com", "/")
+ require.Error(t, err)
+ // newer Go versions return "tls: certificate required"
+ require.ErrorContains(t, err, "tls: bad certificate")
+ require.Nil(t, rsp)
+ })
+ t.Run("client without cert works to non-mtls domain", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, noClientCertListener, "group.gitlab-example.com", "/")
+
+ require.NoError(t, err)
+ require.NoError(t, rsp.Body.Close())
+ require.EqualValues(t, http.StatusOK, rsp.StatusCode)
+ })
+ t.Run("client with cert works to non-mtls domain", func(t *testing.T) {
+ rsp, err := GetPageFromListener(t, clientCertListener, "group.gitlab-example.com", "/")
+
+ require.NoError(t, err)
+ require.NoError(t, rsp.Body.Close())
+ require.EqualValues(t, http.StatusOK, rsp.StatusCode)
+ })
+}
+
func TestAcceptsSupportedCiphers(t *testing.T) {
RunPagesProcess(t,
withListeners([]ListenSpec{httpsListener}),
diff --git a/test/gitlabstub/api_responses.go b/test/gitlabstub/api_responses.go
index 87233969..3875ae96 100644
--- a/test/gitlabstub/api_responses.go
+++ b/test/gitlabstub/api_responses.go
@@ -50,12 +50,19 @@ type Response struct {
rootDirectory string
}
-func (responses Responses) virtualDomain(wd string) api.VirtualDomain {
- return api.VirtualDomain{
- Certificate: "",
- Key: "",
- LookupPaths: responses.lookupPaths(wd),
+func (responses Responses) virtualDomain(host, wd string) api.VirtualDomain {
+ vd := api.VirtualDomain{
+ Certificate: "",
+ Key: "",
+ ClientCertificate: "",
+ LookupPaths: responses.lookupPaths(wd),
}
+ if found, ok := tlsSettings[host]; ok {
+ vd.Certificate = found.Certificate
+ vd.Key = found.Key
+ vd.ClientCertificate = found.ClientCertificate
+ }
+ return vd
}
func (responses Responses) lookupPaths(wd string) []api.LookupPath {
@@ -88,6 +95,14 @@ func (response Response) lookupPath(prefix, wd string) api.LookupPath {
}
}
+var tlsSettings = map[string]api.VirtualDomain{
+ "mtls.gitlab-example.com": {
+ Certificate: "-----BEGIN CERTIFICATE-----\nMIIDZDCCAkygAwIBAgIRAOtN9/zy+gFjdsgpKq3QRdQwDQYJKoZIhvcNAQELBQAw\nMzEUMBIGA1UEChMLTG9nIENvdXJpZXIxGzAZBgNVBAMTEmdpdGxhYi1leGFtcGxl\nLmNvbTAgFw0xODAzMjMxODMwMDZaGA8yMTE4MDIyNzE4MzAwNlowMzEUMBIGA1UE\nChMLTG9nIENvdXJpZXIxGzAZBgNVBAMTEmdpdGxhYi1leGFtcGxlLmNvbTCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKULsxpnazXX5RsVzayrAQB+lWwr\nWef5L5eDhSsIsBLbelYp5YB4TmVRt5x7bWKOOJSBsOfwHZHKJXdu+uuX2RenZlhk\n3Qpq9XGaPZjYm/NHi8gBHPAtz5sG5VaKNvkfTzRGnO9CWA9TM1XtYiOBq94dO+H3\nc+5jP5Yw+mJ+hA+i2058zF8nRlUHArEno2ofrHwE0LMZ11VskpXtWnVfs3voLs8p\nr76KXPBFkMJR4qkWrMDF5Y5MbsQ0zisn6KXrTyV0S4MQh4vSyPdFHnEzvJ07rm5x\n4RTWrjgQeQ2DjZjQvRmaDzlVBK9kaMkJ1Si3agK+gpji6d6WZ/Mb2el1GK8CAwEA\nAaNxMG8wDgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud\nEwEB/wQFMAMBAf8wNwYDVR0RBDAwLoIUKi5naXRsYWItZXhhbXBsZS5jb22HBH8A\nAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBAJ0NM8apK0xI\nYxMstP/dCQXtR0wyREGSD/eOpeY3bWlqCbpRgMFUGjQlrsEozcPZOCSCKX5p+tym\n7GsnYtXkwbsuURoSz+5IlhRPVHcUlUeGRdv3/gCd8fDXiigALCsB6GrkMG5cUfh+\nx5p52AC3eQdWTDoxNou+2gzwkAl8iJc13Ykusst0YUqcsXKqTuei2quxFv0pEBSO\np8wEixoicLFNqPnIDmgx5894DAn0bccNXgRWtq8lLbdhGUlBbpatevvFMgNvFUbe\neeGb9D0EfpxmzxUl+L0xZtfg3f7cu5AgLG8tb6l4AK6NPVuXN8DmUgvnauWJjZME\nfgStI+IRNVg=\n-----END CERTIFICATE-----",
+ Key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApQuzGmdrNdflGxXNrKsBAH6VbCtZ5/kvl4OFKwiwEtt6Vinl\ngHhOZVG3nHttYo44lIGw5/Adkcold27665fZF6dmWGTdCmr1cZo9mNib80eLyAEc\n8C3PmwblVoo2+R9PNEac70JYD1MzVe1iI4Gr3h074fdz7mM/ljD6Yn6ED6LbTnzM\nXydGVQcCsSejah+sfATQsxnXVWySle1adV+ze+guzymvvopc8EWQwlHiqRaswMXl\njkxuxDTOKyfopetPJXRLgxCHi9LI90UecTO8nTuubnHhFNauOBB5DYONmNC9GZoP\nOVUEr2RoyQnVKLdqAr6CmOLp3pZn8xvZ6XUYrwIDAQABAoIBAHhP5QnUZeTkMtDh\nvgKmzZ4sqIQnvexKTBUo/MR4GtJESBPTisdx68QUI8LgfsafYkNvnyQUd5m1QEam\nEif3k3uYvhSlwjQ78BwWEdz/2f8oIo9zsEKtQm+CQWAqdRR5bGVxLCmFtWfGgN+c\nojO77SuHKAX7OvmGQ+4aWgu+qkoyg/chIpPXMduAjLMtN3eg60ZqJ5KrKuIF63Bb\nxkPQvzJueB9SfUurmKjUltDMx6G/9RZyS0OIRGyL9Qp8MZ8jE23cXOcDgm0HhkPq\nW4LU++aWAOLYziTjnhjJ+4Iz9R7U8sCmk1wgnK/tapVcJf41R98WuGluyjXpsXgA\nk7vmofECgYEAzuGun9lZ7xGwPifp6vGanWXnW+JiZgCTGuHWgQLIXWYcLfoI3kpH\neLBYINBwvjIQ7P6UxsGSSXd+T82t+8W2LLc2fiKFKE1LVySpH99+cfmIPXxrviOz\nGBX9LTdSCdGkgb54m8aJCpNFnKw5wYgcW1L8CaXXly2Z/KNrGR9R/YUCgYEAzDs4\n19HqlutGLTC30/ziiiIfDaBbX9AzBdUfp9GdT53Mi/7bfxpW/sL4RjG2fGgmN6ua\nfh5npT9AB1ldcEg2qfyOJPt1Ubdi6ek9lx8AB2RMhwdihgX+7bjVMFtjg4b8z5C1\njQbEr1rhFdpaGyNehtAXDgCbDWQBYnBrmM0rCaMCgYBip1Qyfd9ZFcJJoZb2pofo\njvOo6Weq5JNBungjxUPu5gaCFj2sYxd6Af3EiCF7UTypBy3DKgOsbQMa4yYYbcvV\nvviJZcTB1zoaMC1GObl+eFPzniVy4mtBDRtSOJMyg3pDNKUnA6HOHTSQ5cAU/ecn\n1YbCwwbv3JsV0of7zue2UQKBgQCVc0j3dd9rLSQfcaUz9bx5RNrgh9YV2S9dN0aA\n8f1iA6FpWMiazFWY/GfeRga6JyTAXE0juXAzFoPuXNDpl46Y+f2yxmhlsgMqFMpD\nSiYlQppVvWu1k7GnmDg5uMarux5JbiXM24UWpTRNX4nMjidgE+qrDnpoZCQ3Ovkh\nyhGSbQKBgD3VEnPiSUmXBo39kPcnPg93E3JfdAOiOwIB2qwfYzg9kpmuTWws+DFz\nlKpMI27YkmnPqROQ2NTUfdxYmw3EHHMAsvnmHeMNGn3ijSUZVKmPfV436Qc8iVci\ns4wKoCRhBUZ52sHki/ieb+5hycT3JnVXMDtbJxgXFW5a86usXEpO\n-----END RSA PRIVATE KEY-----",
+ ClientCertificate: "-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n",
+ },
+}
+
// apiResponses holds the predefined API responses for certain domains
// that can be used with the GitLab API stub in acceptance tests
var apiResponses = APIResponses{
@@ -185,6 +200,9 @@ var apiResponses = APIResponses{
"/project5": {
pathOnDisk: "group.https-only/project5",
},
+ "/project6": {
+ pathOnDisk: "group.https-only/project6",
+ },
},
"withacmechallenge.domain.com": {
"/": {
@@ -280,6 +298,13 @@ var apiResponses = APIResponses{
pathOnDisk: "group.acme/with.redirects",
},
},
+ "mtls.gitlab-example.com": {
+ "/": {
+ projectID: 1009,
+ httpsOnly: true,
+ pathOnDisk: "group.https-only/project6",
+ },
+ },
"group.unique-url.gitlab-example.com": {
"/with-unique-url": {
uniqueHost: "unique-url-group-unique-url-a1b2c3d4e5f6.gitlab-example.com",
diff --git a/test/gitlabstub/handlers.go b/test/gitlabstub/handlers.go
index 46cff818..435ada86 100644
--- a/test/gitlabstub/handlers.go
+++ b/test/gitlabstub/handlers.go
@@ -28,7 +28,7 @@ func defaultAPIHandler(delay time.Duration, pagesRoot string) http.HandlerFunc {
// check if predefined response exists
if responses, ok := apiResponses[domain]; ok {
- if err := json.NewEncoder(w).Encode(responses.virtualDomain(pagesRoot)); err != nil {
+ if err := json.NewEncoder(w).Encode(responses.virtualDomain(domain, pagesRoot)); err != nil {
log.Fatalf("fail to encode response for domain %q: %v", domain, err)
}
return