Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • gitlab-org/gitlab-pages
  • rfwatson/gitlab-pages
  • masakura/gitlab-pages
  • gordio/gitlab-pages
  • zloster/gitlab-pages
  • jonnymbgood/gitlab-pages
  • shinya.maeda/gitlab-pages
  • xer0x/gitlab-pages
  • esabelhaus/gitlab-pages
  • frolvlad/gitlab-pages
  • mgresko/gitlab-pages
  • fzipi/gitlab-pages
  • naysayer1/gitlab-pages
  • yaowenli/gitlab-pages
  • tuomoa/gitlab-pages
  • ahodgen/gitlab-pages
  • techonomics_/gitlab-pages
  • icode1/gitlab-pages
  • Maritonette/gitlab-pages
  • uros.simovic/gitlab-pages
  • randallscott99/gitlab-pages
  • nolith/gitlab-pages
  • Ww3.Google.com/gitlab-pages
  • Ambyjkl/gitlab-pages
  • pknw1/gitlab-pages
  • macstacks/gitlab-pages
  • lpasselin/gitlab-pages
  • itoijala/gitlab-pages
  • JonathonReinhart/gitlab-pages
  • Azaradel/gitlab-pages
  • fundasecgin32/gitlab-pages
  • BageDevimo/gitlab-pages
  • jmkim/gitlab-pages
  • siemens/gitlab-pages
  • tardyp/gitlab-pages
  • michaels3d/gitlab-pages
  • tuxillo/gitlab-pages
  • karrick/gitlab-pages
  • sca_gitlab/gitlab-pages
  • alerque/gitlab-pages
  • unaheidi/gitlab-pages
  • maryomotayo2/gitlab-pages
  • liuzyhn/gitlab-pages
  • xingdi25/gitlab-pages
  • chefashiful.islam/gitlab-pages
  • johan.ramirez/gitlab-pages
  • Laurenslill/gitlab-pages
  • yigithankardas/gitlab-pages
  • leolara/gitlab-pages
  • nuwe1/gitlab-pages
  • GoroD/gitlab-pages
  • PopeDrFreud/gitlab-pages
  • hemanthdev/gitlab-pages
  • ba2014sheer/gitlab-pages
  • NicoleS021/gitlab-pages
  • boboso1971/gitlab-pages
  • mark2472-repos/gitlab-pages
  • derekmiller111978.dm/gitlab-pages
  • 229083520/gitlab-pages
  • armbiant/gitlab-pages
  • 358253885/gitlab-pages
  • seavhong0001/gitlab-pages
  • ashimtriv/gitlab-pages
  • ayushontop/gitlab-pages
  • jaime/gitlab-pages
  • sdwolfz/gitlab-pages
  • mipmip/gitlab-pages
  • ksashikumar/gitlab-pages
  • poppygo/gitlab-pages
  • xmarlem/gitlab-pages
  • realFlowControl/gitlab-pages
  • feistel/gitlab-pages
  • Shefali321/gitlab-pages
  • we88c0de/gitlab-pages
  • sathieu/gitlab-pages
  • bhuvantmey/gitlab-pages
  • kevin.kengne1/gitlab-pages
  • Dishon/gitlab-pages
  • ljdo.lj/gitlab-pages
  • ercan.ucan/gitlab-pages
  • davidleger95/gitlab-pages
  • jenslauterbach/gitlab-pages
  • kinohub1/gitlab-pages
  • psykomal/gitlab-pages
  • d.esterman/gitlab-pages
  • nfriend/gitlab-pages
  • ax10336/gitlab-pages
  • bhiseviraj/gitlab-pages
  • Virajbhise/gitlab-pages
  • nikovega21/gitlab-pages
  • HaroldKnowlden/gitlab-pages
  • mlegner/gitlab-pages
  • lenharo/gitlab-pages
  • danscott/gitlab-pages
  • Kolan92/gitlab-pages
  • HuseyinEmreAksoy/gitlab-pages
  • test_jaime/gitlab-pages
  • Osmanilge/gitlab-pages
  • cs-ic/gitlab-pages
  • TamerlanG1/gitlab-pages
  • aravind.mylsamy/gitlab-pages
  • quiqr/gitlab-pages
  • andrew.c3/gitlab-pages
  • altiayirem/gitlab-pages
  • sayeedahmad/gitlab-pages
  • chriscrabtree2021/gitlab-pages
  • leshkatruhachev/gitlab-pages
  • l.dijkman/gitlab-pages
  • casfahrenfort/gitlab-pages
  • dtjdtrj/gitlab-pages
  • kevin.rojas/wr-gitlab-pages
  • gitlab-renovate-forks/gitlab-pages
  • slumericanbds1/gitlab-pages
  • ayuryshev/gitlab-pages
  • abitrolly/gitlab-pages
  • sitedata/gitlab-pages
  • tidys/gitlab-pages
  • Mohamad.Elsuity/gitlab-pages
  • principallksk/gitlab-pages
  • gitlab-community/gitlab-pages
  • rafaelgamboatumtum/gitlab-pages
  • armbiant/gnome-gitlab-pages
  • ollevche/gitlab-pages
  • xMoelletschi/gitlab-pages
  • mukerolas25/gitlab-pages
125 results
Show changes
Commits on Source (2)
Showing
with 670 additions and 45 deletions
......@@ -387,7 +387,7 @@ func runApp(config *cfg.Config) error {
return fmt.Errorf("failed to initialize logging: %w", err)
}
a.Artifact = artifact.New(config.ArtifactsServer.URL, config.ArtifactsServer.TimeoutSeconds, config.General.Domain)
a.Artifact = artifact.New(config.ArtifactsServer.URL, config.ArtifactsServer.TimeoutSeconds, config.General.Domain, config.GitLab.ClientCfg)
if err := a.setAuth(config); err != nil {
return err
......@@ -426,6 +426,7 @@ func (a *theApp) setAuth(config *cfg.Config) error {
AuthTimeout: config.Authentication.Timeout,
CookieSessionTimeout: config.Authentication.CookieSessionTimeout,
AllowNamespaceInPath: config.General.NamespaceInPath,
ClientCfg: config.GitLab.ClientCfg,
})
if err != nil {
return fmt.Errorf("could not initialize auth package: %w", err)
......
......@@ -13,6 +13,7 @@ import (
"strings"
"time"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
"gitlab.com/gitlab-org/gitlab-pages/internal/errortracking"
"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
"gitlab.com/gitlab-org/gitlab-pages/internal/httptransport"
......@@ -47,13 +48,15 @@ type Artifact struct {
// New when provided the arguments defined herein, returns a pointer to an
// Artifact that is used to proxy requests.
func New(server string, timeoutSeconds int, pagesDomain string) *Artifact {
func New(server string, timeoutSeconds int, pagesDomain string, clientCfg config.HTTPClientCfg) *Artifact {
httpTransport := httptransport.NewTransportWithClientCert(clientCfg)
return &Artifact{
server: strings.TrimRight(server, "/"),
suffix: "." + strings.ToLower(pagesDomain),
client: &http.Client{
Timeout: time.Second * time.Duration(timeoutSeconds),
Transport: httptransport.DefaultTransport,
Transport: httpTransport,
},
}
}
......
......@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-pages/internal/artifact"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
)
func TestTryMakeRequest(t *testing.T) {
......@@ -107,7 +108,7 @@ func TestTryMakeRequest(t *testing.T) {
r.RemoteAddr = c.RemoteAddr
art := artifact.New(testServer.URL, 1, "gitlab-example.io")
art := artifact.New(testServer.URL, 1, "gitlab-example.io", config.HTTPClientCfg{})
result := httptest.NewRecorder()
......@@ -301,7 +302,7 @@ func TestBuildURL(t *testing.T) {
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
a := artifact.New(c.RawServer, 1, c.PagesDomain)
a := artifact.New(c.RawServer, 1, c.PagesDomain, config.HTTPClientCfg{})
u, ok := a.BuildURL(c.Host, c.Path)
msg := c.Description + " - generated URL: "
......@@ -332,7 +333,7 @@ func TestContextCanceled(t *testing.T) {
r = r.WithContext(ctx)
// cancel context explicitly
cancel()
art := artifact.New(testServer.URL, 1, "gitlab-example.io")
art := artifact.New(testServer.URL, 1, "gitlab-example.io", config.HTTPClientCfg{})
require.True(t, art.TryMakeRequest(result, r, "", func(resp *http.Response) bool { return false }))
require.Equal(t, http.StatusNotFound, result.Code)
......
......@@ -21,6 +21,7 @@ import (
"golang.org/x/crypto/hkdf"
"gitlab.com/gitlab-org/gitlab-pages/internal"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
"gitlab.com/gitlab-org/gitlab-pages/internal/errortracking"
"gitlab.com/gitlab-org/gitlab-pages/internal/feature"
"gitlab.com/gitlab-org/gitlab-pages/internal/httperrors"
......@@ -710,6 +711,7 @@ type Options struct {
AuthTimeout time.Duration
CookieSessionTimeout time.Duration
AllowNamespaceInPath bool
ClientCfg config.HTTPClientCfg
}
// New when authentication supported this will be used to create authentication handler
......@@ -719,6 +721,7 @@ func New(options *Options) (*Auth, error) {
if err != nil {
return nil, err
}
httpTransport := httptransport.NewTransportWithClientCert(options.ClientCfg)
return &Auth{
pagesDomain: options.PagesDomain,
......@@ -729,7 +732,7 @@ func New(options *Options) (*Auth, error) {
publicGitlabServer: strings.TrimRight(options.PublicGitlabServer, "/"),
apiClient: &http.Client{
Timeout: options.AuthTimeout,
Transport: httptransport.DefaultTransport,
Transport: httpTransport,
},
store: sessions.NewCookieStore(keys[0], keys[1]),
authSecret: options.StoreSecret,
......
......@@ -119,6 +119,14 @@ type GitLab struct {
JWTTokenExpiration time.Duration
Cache Cache
EnableDisk bool
ClientCfg HTTPClientCfg
}
type HTTPClientCfg struct {
CAFiles []string
Certs []tls.Certificate
MinVersion uint16
MaxVersion uint16
}
// Log groups settings related to configuring logging
......@@ -422,6 +430,18 @@ func loadConfig() (*Config, error) {
}
}
certs, err := loadClientCerts(clientCerts.Split())
if err != nil {
return nil, err
}
config.GitLab.ClientCfg = HTTPClientCfg{
Certs: certs,
CAFiles: caCerts.Split(),
MinVersion: allTLSVersions[*tlsMinVersion],
MaxVersion: allTLSVersions[*tlsMaxVersion],
}
customHeaders, err := parseHeaderString(header.Split())
if err != nil {
return nil, fmt.Errorf("unable to parse header string: %w", err)
......@@ -449,6 +469,30 @@ func loadConfig() (*Config, error) {
return config, nil
}
func loadClientCerts(certs []string) ([]tls.Certificate, error) {
c := make([]tls.Certificate, 0, len(certs))
for i, pair := range certs {
sep := strings.Index(pair, ":")
if sep == -1 {
return nil, fmt.Errorf("malformed client certs at position %d: %w", i, errMalformedClientCert)
}
cert := pair[:sep]
key := pair[sep+1:]
tlsCert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, err
}
c = append(c, tlsCert)
}
return c, nil
}
func logFields(config *Config) map[string]any {
return map[string]any{
"artifacts-server": config.ArtifactsServer.URL,
......@@ -522,6 +566,7 @@ func logFields(config *Config) map[string]any {
"sentry-environment": config.Sentry.Environment,
"version": config.General.ShowVersion,
"namespace-in-path": *namespaceInPath,
"ca-certs": config.GitLab.ClientCfg.CAFiles,
}
}
......
......@@ -107,6 +107,9 @@ var (
header = MultiStringFlag{separator: ";;"}
clientCerts = MultiStringFlag{separator: ","}
caCerts = MultiStringFlag{separator: ","}
namespaceInPath = flag.Bool("namespace-in-path", false, "Enable Namespace in path")
// flags that won't be logged to the output on Pages boot
......@@ -126,6 +129,8 @@ func initFlags() {
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")
flag.Var(&clientCerts, "client-cert-key-pairs", "File paths to client certificate and key PEM files to use for mutual TLS")
flag.Var(&caCerts, "ca-certs", "File paths to CA certificates used to sign client certificates for mutual TLS")
// read from -config=/path/to/gitlab-pages-config
flag.String(flag.DefaultConfigFlagname, "", "path to config file")
......
......@@ -18,6 +18,7 @@ var (
errArtifactsServerUnsupportedScheme = errors.New("artifacts-server scheme must be either http:// or https://")
errArtifactsServerInvalidTimeout = errors.New("artifacts-server-timeout must be greater than or equal to 1")
errEmptyListener = errors.New("listener must not be empty")
errMalformedClientCert = errors.New("client certificates must be defined as /path/to/cert:/path/to/key")
)
// Validate values populated in Config
......
......@@ -5,10 +5,13 @@ import (
"crypto/x509"
"net"
"net/http"
"os"
"sync"
"time"
"gitlab.com/gitlab-org/labkit/log"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
)
const (
......@@ -53,6 +56,50 @@ func NewTransport() *http.Transport {
}
}
// NewTransportWithClientCert creates a new http.Transport with the provided client certificate configuration.
// It sets up the TLS configuration with the provided CA files and client certificates.
// The transport is configured with default connection values such as timeouts and max idle connections.
func NewTransportWithClientCert(clientCfg config.HTTPClientCfg) *http.Transport {
certPool := pool()
for _, caFile := range clientCfg.CAFiles {
cert, err := os.ReadFile(caFile)
if err == nil {
certPool.AppendCertsFromPEM(cert)
} else {
log.WithError(err).WithField("ca-file", caFile).Error("reading CA file")
}
}
tlsConfig := &tls.Config{
RootCAs: certPool,
MinVersion: tls.VersionTLS12, // set MinVersion to fix gosec: G402
}
tlsConfig.MinVersion = clientCfg.MinVersion
tlsConfig.MaxVersion = clientCfg.MaxVersion
if clientCfg.Certs != nil {
tlsConfig.Certificates = clientCfg.Certs
}
return &http.Transport{
DialTLS: func(network, addr string) (net.Conn, error) {
return tls.Dial(network, addr, tlsConfig)
},
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
// overrides the DefaultMaxIdleConnsPerHost = 2
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
// Set more timeouts https://gitlab.com/gitlab-org/gitlab-pages/-/issues/495
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
ExpectContinueTimeout: 15 * time.Second,
}
}
// This is here because macOS does not support the SSL_CERT_FILE and
// SSL_CERT_DIR environment variables. We have arranged things to read
// SSL_CERT_FILE and SSL_CERT_DIR as late as possible to avoid conflicts
......
......@@ -44,7 +44,7 @@ type Client struct {
// NewClient initializes and returns new Client baseUrl is
// appConfig.InternalGitLabServer secretKey is appConfig.GitLabAPISecretKey
func NewClient(baseURL string, secretKey []byte, connectionTimeout, jwtTokenExpiry time.Duration) (*Client, error) {
func NewClient(baseURL string, secretKey []byte, connectionTimeout, jwtTokenExpiry time.Duration, clientCfg config.HTTPClientCfg) (*Client, error) {
if len(baseURL) == 0 || len(secretKey) == 0 {
return nil, errors.New("GitLab API URL or API secret has not been provided")
}
......@@ -62,6 +62,8 @@ func NewClient(baseURL string, secretKey []byte, connectionTimeout, jwtTokenExpi
return nil, errors.New("GitLab JWT token expiry has not been provided")
}
httpTransport := httptransport.NewTransportWithClientCert(clientCfg)
return &Client{
secretKey: secretKey,
baseURL: parsedURL,
......@@ -69,7 +71,7 @@ func NewClient(baseURL string, secretKey []byte, connectionTimeout, jwtTokenExpi
Timeout: connectionTimeout,
Transport: httptransport.NewMeteredRoundTripper(
correlation.NewInstrumentedRoundTripper(
httptransport.DefaultTransport,
httpTransport,
correlation.WithClientName(transportClientName),
),
transportClientName,
......@@ -85,7 +87,7 @@ func NewClient(baseURL string, secretKey []byte, connectionTimeout, jwtTokenExpi
// NewFromConfig creates a new client from Config struct
func NewFromConfig(cfg *config.GitLab) (*Client, error) {
return NewClient(cfg.InternalServer, cfg.APISecretKey, cfg.ClientHTTPTimeout, cfg.JWTTokenExpiration)
return NewClient(cfg.InternalServer, cfg.APISecretKey, cfg.ClientHTTPTimeout, cfg.JWTTokenExpiration, cfg.ClientCfg)
}
// Resolve returns a VirtualDomain configuration wrapped into a Lookup for a
......
......@@ -2,6 +2,7 @@ package client
import (
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net/http"
......@@ -14,8 +15,10 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-pages/internal/config"
"gitlab.com/gitlab-org/gitlab-pages/internal/domain"
"gitlab.com/gitlab-org/gitlab-pages/internal/fixture"
"gitlab.com/gitlab-org/gitlab-pages/internal/testhelpers"
)
const (
......@@ -59,7 +62,7 @@ func TestConnectionReuse(t *testing.T) {
}
func TestNewValidBaseURL(t *testing.T) {
_, err := NewClient("https://gitlab.com", secretKey(t), defaultClientConnTimeout, defaultJWTTokenExpiry)
_, err := NewClient("https://gitlab.com", secretKey(t), defaultClientConnTimeout, defaultJWTTokenExpiry, config.HTTPClientCfg{})
require.NoError(t, err)
}
......@@ -129,7 +132,7 @@ func TestNewInvalidConfiguration(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewClient(tt.args.baseURL, tt.args.secretKey, tt.args.connectionTimeout, tt.args.jwtTokenExpiry)
got, err := NewClient(tt.args.baseURL, tt.args.secretKey, tt.args.connectionTimeout, tt.args.jwtTokenExpiry, config.HTTPClientCfg{})
require.Nil(t, got)
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErrMsg)
......@@ -233,6 +236,49 @@ func TestGetVirtualDomainAuthenticatedRequest(t *testing.T) {
require.Equal(t, "mygroup/myproject/public/", lookupPath.Source.Path)
}
func TestMutualTLSClientAuthentication(t *testing.T) {
gitlabMux := http.NewServeMux()
gitlabMux.HandleFunc("/api/v4/internal/pages", func(w http.ResponseWriter, r *http.Request) {
// Ensure that pagesClient certificate is present
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
w.WriteHeader(http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
})
certPath := "../../../../test/testdata/mutualtls/valid/client.crt"
keyPath := "../../../../test/testdata/mutualtls/valid/client.key"
caCertPath := "../../../../test/testdata/mutualtls/valid/ca.crt"
tlsCfg := &tls.Config{
ClientCAs: testhelpers.CertPool(t, caCertPath),
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{testhelpers.Cert(t, certPath, keyPath)},
MinVersion: tls.VersionTLS12,
}
server := httptest.NewUnstartedServer(gitlabMux)
server.TLS = tlsCfg
server.StartTLS()
defer server.Close()
hcc := config.HTTPClientCfg{Certs: []tls.Certificate{testhelpers.Cert(t, certPath, keyPath)}, CAFiles: []string{caCertPath}}
pagesClient, err := NewClient(server.URL, secretKey(t), defaultClientConnTimeout, defaultJWTTokenExpiry, hcc)
require.NoError(t, err)
params := url.Values{}
params.Set("host", "group.gitlab.io")
resp, err := pagesClient.get(context.Background(), "/api/v4/internal/pages", params)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
}
func validateToken(t *testing.T, tokenString string) {
t.Helper()
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
......@@ -263,7 +309,7 @@ func secretKey(t *testing.T) []byte {
func defaultClient(t *testing.T, url string) *Client {
t.Helper()
client, err := NewClient(url, secretKey(t), defaultClientConnTimeout, defaultJWTTokenExpiry)
client, err := NewClient(url, secretKey(t), defaultClientConnTimeout, defaultJWTTokenExpiry, config.HTTPClientCfg{})
require.NoError(t, err)
return client
......@@ -324,7 +370,7 @@ func Test_endpoint(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
gc, err := NewClient(tt.basePath, []byte("secret"), defaultClientConnTimeout, defaultJWTTokenExpiry)
gc, err := NewClient(tt.basePath, []byte("secret"), defaultClientConnTimeout, defaultJWTTokenExpiry, config.HTTPClientCfg{})
require.NoError(t, err)
got, err := gc.endpoint(tt.urlPath, tt.params)
......
package testhelpers
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
......@@ -110,3 +112,32 @@ func Close(t *testing.T, c io.Closer) {
require.NoError(t, c.Close())
})
}
// CertPool creates a new certificate pool containing the certificate.
func CertPool(tb testing.TB, certPath string) *x509.CertPool {
tb.Helper()
pem := MustReadFile(tb, certPath)
pool := x509.NewCertPool()
require.True(tb, pool.AppendCertsFromPEM(pem))
return pool
}
// Cert returns the parsed certificate.
func Cert(tb testing.TB, certPath, keyPath string) tls.Certificate {
tb.Helper()
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
require.NoError(tb, err)
return cert
}
// MustReadFile returns the content of a file or fails at once.
func MustReadFile(tb testing.TB, filename string) []byte {
tb.Helper()
content, err := os.ReadFile(filename)
if err != nil {
tb.Fatal(err)
}
return content
}
......@@ -266,3 +266,161 @@ func TestPrivateArtifactProxyRequest(t *testing.T) {
})
}
}
type testCase struct {
name string
host string
path string
status int
content string
length int64
cacheControl string
contentType string
}
func testArtifactProxyRequestWithMTLS(t *testing.T, content string, tests []testCase, clientCertPath, clientKeyPath, caCertPath string) {
t.Helper()
testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.RawPath {
case "/api/v4/projects/group%2Fproject/jobs/1/artifacts/200.html",
"/api/v4/projects/group%2Fsubgroup%2Fproject/jobs/1/artifacts/200.html":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, content)
default:
t.Logf("Unexpected r.URL.RawPath: %q", r.URL.RawPath)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, content)
}
}))
keyFile, certFile := CreateHTTPSFixtureFiles(t)
serverCert, err := tls.LoadX509KeyPair(certFile, keyFile)
require.NoError(t, err)
tlsCfg := &tls.Config{
ClientCAs: testhelpers.CertPool(t, caCertPath),
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{serverCert, testhelpers.Cert(t, clientCertPath, clientKeyPath)},
MinVersion: tls.VersionTLS12,
}
testServer.TLS = tlsCfg
testServer.StartTLS()
t.Cleanup(func() {
testServer.Close()
})
// Ensure the IP address is used in the URL, as we're relying on IP SANs to
// validate
artifactServerURL := testServer.URL + "/api/v4"
t.Log("Artifact server URL", artifactServerURL)
args := []string{"-artifacts-server=" + artifactServerURL, "-artifacts-server-timeout=1"}
t.Setenv("SSL_CERT_FILE", certFile)
clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
require.NoError(t, err)
caCert, err := loadCACertificate(caCertPath)
require.NoError(t, err)
RunPagesProcess(t,
withListeners([]ListenSpec{httpListener}),
withArguments(args),
withExtraArgument("client-cert-key-pairs", clientCertPath+":"+clientKeyPath),
withExtraArgument("ca-certs", caCertPath),
withStubOptions(gitlabstub.WithCertificate(clientCert), gitlabstub.WithMutualTLS(caCert)),
)
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
resp, err := GetPageFromListener(t, httpListener, tt.host, tt.path)
require.NoError(t, err)
testhelpers.Close(t, resp.Body)
require.Equal(t, tt.status, resp.StatusCode)
require.Equal(t, tt.contentType, resp.Header.Get("Content-Type"))
if tt.status == http.StatusOK {
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, tt.content, string(body))
require.Equal(t, tt.length, resp.ContentLength)
require.Equal(t, tt.cacheControl, resp.Header.Get("Cache-Control"))
}
})
}
}
func TestArtifactProxyRequestWithValidMTLS(t *testing.T) {
content := "<!DOCTYPE html><html><head><title>Title of the document</title></head><body></body></html>"
contentLength := int64(len(content))
clientCertPath := "../../test/testdata/mutualtls/valid/client.crt"
clientKeyPath := "../../test/testdata/mutualtls/valid/client.key"
caCertPath := "../../test/testdata/mutualtls/valid/ca.crt"
tests := []testCase{
{
name: "basic proxied request",
host: "group.gitlab-example.com",
path: "/-/project/-/jobs/1/artifacts/200.html",
status: http.StatusOK,
content: content,
length: contentLength,
cacheControl: "max-age=3600",
contentType: "text/html; charset=utf-8",
},
{
name: "basic proxied request for subgroup",
host: "group.gitlab-example.com",
path: "/-/subgroup/project/-/jobs/1/artifacts/200.html",
status: http.StatusOK,
content: content,
length: contentLength,
cacheControl: "max-age=3600",
contentType: "text/html; charset=utf-8",
},
}
testArtifactProxyRequestWithMTLS(t, content, tests, clientCertPath, clientKeyPath, caCertPath)
}
func TestArtifactProxyRequestWithInvalidMTLS(t *testing.T) {
content := "<!DOCTYPE html><html><head><title>Title of the document</title></head><body></body></html>"
clientCertPath := "../../test/testdata/mutualtls/invalid/client.crt"
clientKeyPath := "../../test/testdata/mutualtls/invalid/client.key"
caCertPath := "../../test/testdata/mutualtls/invalid/ca.crt"
tests := []testCase{
{
name: "basic proxied request",
host: "group.gitlab-example.com",
path: "/-/project/-/jobs/1/artifacts/200.html",
status: http.StatusBadGateway,
content: "",
length: 0,
cacheControl: "",
contentType: "text/html; charset=utf-8",
},
{
name: "basic proxied request for subgroup",
host: "group.gitlab-example.com",
path: "/-/subgroup/project/-/jobs/1/artifacts/200.html",
status: http.StatusBadGateway,
content: "",
length: 0,
cacheControl: "",
contentType: "text/html; charset=utf-8",
},
}
testArtifactProxyRequestWithMTLS(t, content, tests, clientCertPath, clientKeyPath, caCertPath)
}
......@@ -635,6 +635,56 @@ func TestHijackedCode(t *testing.T) {
require.Equal(t, impersonatingRes.StatusCode, http.StatusInternalServerError, "should fail to decode code")
}
func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorizeWithValidMTLS(t *testing.T) {
clientCertPath := "../../test/testdata/mutualtls/valid/client.crt"
clientKeyPath := "../../test/testdata/mutualtls/valid/client.key"
caCertPath := "../../test/testdata/mutualtls/valid/ca.crt"
RunPagesProcessWithMutualTLS(t, httpsListener, defaultAuthConfig(t), clientCertPath, clientKeyPath, caCertPath)
rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/")
require.NoError(t, err)
testhelpers.Close(t, rsp.Body)
require.Equal(t, http.StatusFound, rsp.StatusCode)
require.Len(t, rsp.Header["Location"], 1)
url, err := url.Parse(rsp.Header.Get("Location"))
require.NoError(t, err)
rsp, err = GetRedirectPage(t, httpsListener, url.Host, url.Path+"?"+url.RawQuery)
require.NoError(t, err)
testhelpers.Close(t, rsp.Body)
require.Equal(t, http.StatusFound, rsp.StatusCode)
require.Len(t, rsp.Header["Location"], 1)
url, err = url.Parse(rsp.Header.Get("Location"))
require.NoError(t, err)
require.Equal(t, "https", url.Scheme)
require.Equal(t, "public-gitlab-auth.com", url.Host)
require.Equal(t, "/oauth/authorize", url.Path)
require.Equal(t, "clientID", url.Query().Get("client_id"))
require.Equal(t, "https://projects.gitlab-example.com/auth", url.Query().Get("redirect_uri"))
require.NotEmpty(t, url.Query().Get("scope"))
require.NotEmpty(t, url.Query().Get("state"))
}
func TestWhenAuthIsEnabledPrivateWillRedirectToAuthorizeWithInvalidMTLS(t *testing.T) {
clientCertPath := "../../test/testdata/mutualtls/invalid/client.crt"
clientKeyPath := "../../test/testdata/mutualtls/invalid/client.key"
caCertPath := "../../test/testdata/mutualtls/invalid/ca.crt"
RunPagesProcessWithMutualTLS(t, httpsListener, defaultAuthConfig(t), clientCertPath, clientKeyPath, caCertPath)
rsp, err := GetRedirectPage(t, httpsListener, "group.auth.gitlab-example.com", "private.project/")
require.NoError(t, err)
testhelpers.Close(t, rsp.Body)
require.Equal(t, http.StatusBadGateway, rsp.StatusCode)
}
func getValidCookieAndState(t *testing.T, domain string) (string, string) {
t.Helper()
......
package acceptance_test
import (
_ "embed"
"net/http"
"testing"
"github.com/stretchr/testify/require"
)
func TestGitLabAPIMutualTLS(t *testing.T) {
tests := []struct {
name string
host string
path string
clientCertPath string
clientKeyPath string
caCertPath string
status int
expectError bool
}{
{
name: "basic request works with GitLab API mutual TLS",
host: "group.gitlab-example.com",
path: "/index.html",
clientCertPath: "../../test/testdata/mutualtls/valid/client.crt",
clientKeyPath: "../../test/testdata/mutualtls/valid/client.key",
caCertPath: "../../test/testdata/mutualtls/valid/ca.crt",
status: http.StatusOK,
expectError: false,
},
{
name: "502 when invalid mutual TLS is used",
host: "group.gitlab-example.com",
path: "/index.html",
clientCertPath: "../../test/testdata/mutualtls/invalid/client.crt",
clientKeyPath: "../../test/testdata/mutualtls/invalid/client.key",
caCertPath: "../../test/testdata/mutualtls/invalid/ca.crt",
status: http.StatusBadGateway,
expectError: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
RunPagesProcessWithMutualTLS(t, httpsListener, "", tt.clientCertPath, tt.clientKeyPath, tt.caCertPath)
rsp, err := GetPageFromListener(t, httpsListener, tt.host, tt.path)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
rsp.Body.Close()
}
require.Equal(t, tt.status, rsp.StatusCode)
})
}
}
......@@ -5,6 +5,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
......@@ -334,6 +335,48 @@ func RunPagesProcessWithSSLCertDir(t *testing.T, listeners []ListenSpec, sslCert
)
}
func RunPagesProcessWithMutualTLS(t *testing.T, listener ListenSpec, configFile string, clientCertPath, clientKeyPath, caCertPath string) {
clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
require.NoError(t, err)
caCert, err := loadCACertificate(caCertPath)
require.NoError(t, err)
if configFile == "" {
configFile = newConfigFile(t)
}
RunPagesProcess(t,
withListeners([]ListenSpec{listener}),
withArguments([]string{
"-config=" + configFile,
}),
withExtraArgument("client-cert-key-pairs", clientCertPath+":"+clientKeyPath),
withExtraArgument("ca-certs", caCertPath),
withStubOptions(gitlabstub.WithCertificate(clientCert), gitlabstub.WithMutualTLS(caCert)),
)
}
func loadCACertificate(certPath string) (*x509.Certificate, error) {
if certPath == "" {
return nil, nil
}
caCertFile, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("error reading CA file: %w", err)
}
block, _ := pem.Decode(caCertFile)
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("error parsing CA certificate: %w", err)
}
return caCert, nil
}
func runPagesProcess(t *testing.T, wait bool, pagesBinary string, listeners []ListenSpec, promPort string, extraArgs ...string) (*LogCaptureBuffer, func()) {
t.Helper()
......
......@@ -3,12 +3,17 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
......@@ -16,40 +21,24 @@ import (
)
var (
pagesRoot = flag.String("pages-root", "shared/pages", "The directory where pages are stored")
keyFile = flag.String("key-file", "", "Path to file certificate")
certFile = flag.String("cert-file", "", "Path to file certificate")
pagesRoot = flag.String("pages-root", "shared/pages", "The directory where pages are stored")
keyFile = flag.String("key-file", "", "Path to file certificate")
certFile = flag.String("cert-file", "", "Path to file certificate")
caCertPath = flag.String("ca-crt", "", "CA certificate for mutual TLS")
)
func main() {
flag.Parse()
var opts []gitlabstub.Option
if *keyFile != "" && *certFile != "" {
log.Printf("Loading key pair: (%s) - (%s)", *certFile, *keyFile)
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
log.Fatalf("error loading certificate: %v", err)
}
opts = append(opts, gitlabstub.WithCertificate(cert))
}
if err := os.Chdir(*pagesRoot); err != nil {
log.Fatalf("error chdir in %s: %v", *pagesRoot, err)
if err := run(); err != nil {
log.Fatal(err)
}
}
wd, err := os.Getwd()
func run() error {
server, err := createServer()
if err != nil {
log.Fatalf("error getting current dir: %v", err)
}
opts = append(opts, gitlabstub.WithPagesRoot(wd))
server, err := gitlabstub.NewUnstartedServer(opts...)
if err != nil {
log.Fatalf("error starting the server: %v", err)
return fmt.Errorf("error starting the server: %w", err)
}
if server.TLS != nil {
......@@ -69,6 +58,73 @@ func main() {
defer cancel()
if err := server.Config.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("error shutting down %v", err)
return fmt.Errorf("error shutting down %w", err)
}
return nil
}
func createServer() (*httptest.Server, error) {
var opts []gitlabstub.Option
if cert, err := loadCertificate(*certFile, *keyFile); err != nil {
return nil, err
} else if cert != nil {
opts = append(opts, gitlabstub.WithCertificate(*cert))
}
if wd, err := filepath.Abs(*pagesRoot); err != nil {
return nil, err
} else if *pagesRoot != "" {
opts = append(opts, gitlabstub.WithPagesRoot(wd))
}
if caCert, err := loadCACertificate(*caCertPath); err != nil {
return nil, err
} else if caCert != nil {
opts = append(opts, gitlabstub.WithMutualTLS(caCert))
}
server, err := gitlabstub.NewUnstartedServer(opts...)
if err != nil {
return nil, fmt.Errorf("error starting the server: %w", err)
}
return server, err
}
func loadCertificate(cert string, key string) (*tls.Certificate, error) {
if cert == "" && key == "" {
return nil, nil
}
if cert != "" && key != "" {
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
return nil, fmt.Errorf("error loading certificate: %w", err)
}
return &cert, nil
}
return nil, fmt.Errorf("missing certificate or key: cert(%s) key(%s)", cert, key)
}
func loadCACertificate(certPath string) (*x509.Certificate, error) {
if certPath == "" {
return nil, nil
}
caCertFile, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("error reading CA file: %w", err)
}
block, _ := pem.Decode(caCertFile)
caCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("error parsing CA certificate: %w", err)
}
return caCert, nil
}
......@@ -2,6 +2,7 @@ package gitlabstub
import (
"crypto/tls"
"crypto/x509"
"net/http"
"time"
)
......@@ -40,10 +41,22 @@ func WithDelay(delay time.Duration) Option {
}
func WithCertificate(cert tls.Certificate) Option {
return func(c *config) {
if c.tlsConfig == nil {
c.tlsConfig = defaultTLSConfig()
return func(sc *config) {
if sc.tlsConfig == nil {
sc.tlsConfig = defaultTLSConfig()
}
c.tlsConfig.Certificates = append(c.tlsConfig.Certificates, cert)
sc.tlsConfig.Certificates = append(sc.tlsConfig.Certificates, cert)
}
}
func WithMutualTLS(caCert *x509.Certificate) Option {
return func(sc *config) {
if sc.tlsConfig == nil {
sc.tlsConfig = defaultTLSConfig()
}
sc.tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
sc.tlsConfig.ClientCAs = x509.NewCertPool()
sc.tlsConfig.ClientCAs.AddCert(caCert)
}
}
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
-----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-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFWzobEUkcIgOUR81lz2KSmoPKyNI5UraogKlWOESRbHoAoGCCqGSM49
AwEHoUQDQgAE5S6e7bNszpyhc279oRgW8TMytl0IGaPIhMoDF4oUXO+/sOK/cCz5
9II7nkwkk6COrvbh70ft8e9cdsqta5Usvw==
-----END EC PRIVATE KEY-----