mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-15 09:12:43 +02:00
Currently, client certificate validation is performed within the authorize service, after user login. Instead, configure Envoy to perform certificate validation itself, at the time of the initial connection. When a client certificate authority is configured, Envoy will reject any connection attempts that do not present a valid client certificate with a trust chain rooted at the configured certificate authority. For end users without a client certificate configured in their browser, after this change they will see a browser default error page, rather than an HTML error page served by Pomerium. When multiple client CAs are configured for different routes on the same domain, we will create a bundle from these client CAs, so that a certificate issued by any of these CAs will be accepted during the initial connection. If the presented certificate is not valid for the specific route, then we serve an HTTP 495 response. Add a separate method buildDownstreamTLSContextWithValidation(), so we can make these changes only for the main HTTP listener, and not for the internal gRPC listener. Move the existing unit tests for buildDownstreamTLSContext() over to test buildDownstreamTLSContextWithValidation() instead. Update the existing Envoy configuration test cases, add unit tests for the new clientCAForDomain() function, and add integration test cases.
183 lines
3.8 KiB
Go
183 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/client"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
var IDP, ClusterType string
|
|
|
|
func TestMain(m *testing.M) {
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
|
|
|
flag.Parse()
|
|
if testing.Verbose() {
|
|
log.Logger = log.Logger.Level(zerolog.DebugLevel)
|
|
} else {
|
|
log.Logger = log.Logger.Level(zerolog.InfoLevel)
|
|
}
|
|
|
|
logger := log.With().Logger()
|
|
ctx := logger.WithContext(context.Background())
|
|
|
|
if err := waitForHealthy(ctx); err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "services not healthy")
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
|
|
setIDPAndClusterType(ctx)
|
|
|
|
status := m.Run()
|
|
os.Exit(status)
|
|
}
|
|
|
|
func getClient() *http.Client {
|
|
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
rootCAs, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
bs, err := os.ReadFile(filepath.Join(".", "tpl", "files", "ca.pem"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
_ = rootCAs.AppendCertsFromPEM(bs)
|
|
|
|
return &http.Client{
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
Transport: &http.Transport{
|
|
DisableKeepAlives: true,
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: rootCAs,
|
|
},
|
|
},
|
|
Jar: jar,
|
|
}
|
|
}
|
|
|
|
func waitForHealthy(ctx context.Context) error {
|
|
client := getClient()
|
|
check := func(endpoint string) error {
|
|
reqCtx, clearTimeout := context.WithTimeout(ctx, time.Second)
|
|
defer clearTimeout()
|
|
|
|
req, err := http.NewRequestWithContext(reqCtx, "GET", endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
if res.StatusCode/100 != 2 {
|
|
return fmt.Errorf("%s unavailable: %s", endpoint, res.Status)
|
|
}
|
|
|
|
log.Info().Int("status", res.StatusCode).Msgf("%s healthy", endpoint)
|
|
|
|
return nil
|
|
}
|
|
|
|
ticker := time.NewTicker(time.Second * 3)
|
|
defer ticker.Stop()
|
|
|
|
endpoints := []string{
|
|
"https://authenticate.localhost.pomerium.io/.well-known/pomerium/jwks.json",
|
|
"https://mock-idp.localhost.pomerium.io/.well-known/jwks.json",
|
|
}
|
|
|
|
for {
|
|
var err error
|
|
for _, endpoint := range endpoints {
|
|
err = check(endpoint)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
log.Ctx(ctx).Info().Err(err).Msg("waiting for healthy")
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func setIDPAndClusterType(ctx context.Context) {
|
|
IDP = "oidc"
|
|
ClusterType = "single"
|
|
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to create docker client")
|
|
return
|
|
}
|
|
|
|
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to retrieve docker containers")
|
|
}
|
|
for _, container := range containers {
|
|
for _, name := range container.Names {
|
|
parts := regexp.MustCompile(`^/(\w+?)-(\w+?)_pomerium.*$`).FindStringSubmatch(name)
|
|
if len(parts) == 3 {
|
|
IDP = parts[1]
|
|
ClusterType = parts[2]
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Info().Str("idp", IDP).Str("cluster-type", ClusterType).Send()
|
|
}
|
|
|
|
func mustParseURL(str string) *url.URL {
|
|
u, err := url.Parse(str)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
func loadCertificate(t *testing.T, certName string) tls.Certificate {
|
|
t.Helper()
|
|
certFile := filepath.Join(".", "tpl", "files", certName+".pem")
|
|
keyFile := filepath.Join(".", "tpl", "files", certName+"-key.pem")
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cert
|
|
}
|