mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
App service use upstream TLS options (#66454)
* feat: app service use upstream tls options * refactor: code review suggestions * refactor(upstreamtls): code review suggestions * test(app): provide missing transport options * chore(app): fix lint
This commit is contained in:
+2
-2
@@ -535,12 +535,12 @@ func (a *AppV3) checkTCPPorts() error {
|
||||
}
|
||||
|
||||
switch {
|
||||
// The scheme of URI is enforced to be "tcp" on purpose. This way in the future we can add
|
||||
// The scheme of URI is enforced to be "tcp" (or "tls") on purpose. This way in the future we can add
|
||||
// multi-port support to web apps without throwing hard errors when a cluster with a multi-port
|
||||
// web app gets downgraded to a version which supports multi-port only for TCP apps.
|
||||
//
|
||||
// For now, we simply ignore the Ports field set on non-TCP apps.
|
||||
case uri.Scheme != "tcp":
|
||||
case uri.Scheme != "tcp" && uri.Scheme != SchemeTLS:
|
||||
return nil
|
||||
case a.Spec.MCP != nil:
|
||||
return trace.BadParameter("TCP app %q cannot specify 'mcp' configuration", a.GetName())
|
||||
|
||||
@@ -236,7 +236,7 @@ func (a *App) checkPorts() error {
|
||||
// web app gets downgraded to a version which supports multi-port only for TCP apps.
|
||||
//
|
||||
// For now, we simply ignore the Ports field set on non-TCP apps.
|
||||
if uri.Scheme != "tcp" {
|
||||
if uri.Scheme != "tcp" && uri.Scheme != types.SchemeTLS {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+20
-16
@@ -22,6 +22,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
@@ -262,6 +263,19 @@ func GetTLSCerts(ca types.CertAuthority) [][]byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// GetX509Certs returns parsed TLS certificates from CA as [x509.Certificate].
|
||||
func GetX509Certs(ca types.CertAuthority) iter.Seq2[*x509.Certificate, error] {
|
||||
pairs := ca.GetTrustedTLSKeyPairs()
|
||||
return func(yield func(*x509.Certificate, error) bool) {
|
||||
for _, pair := range pairs {
|
||||
cert, err := tlsca.ParseCertificatePEM(pair.Cert)
|
||||
if !yield(cert, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSSHCheckingKeys returns SSH public keys from CA
|
||||
func GetSSHCheckingKeys(ca types.CertAuthority) [][]byte {
|
||||
pairs := ca.GetTrustedSSHKeyPairs()
|
||||
@@ -279,12 +293,7 @@ func CertPoolFromCertAuthorities(cas []types.CertAuthority) (*x509.CertPool, int
|
||||
certPool := x509.NewCertPool()
|
||||
count := 0
|
||||
for _, ca := range cas {
|
||||
keyPairs := ca.GetTrustedTLSKeyPairs()
|
||||
if len(keyPairs) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, keyPair := range keyPairs {
|
||||
cert, err := tlsca.ParseCertificatePEM(keyPair.Cert)
|
||||
for cert, err := range GetX509Certs(ca) {
|
||||
if err != nil {
|
||||
return nil, 0, trace.Wrap(err)
|
||||
}
|
||||
@@ -298,17 +307,12 @@ func CertPoolFromCertAuthorities(cas []types.CertAuthority) (*x509.CertPool, int
|
||||
// CertPool returns certificate pools from TLS certificates
|
||||
// set up in the certificate authority
|
||||
func CertPool(ca types.CertAuthority) (*x509.CertPool, error) {
|
||||
keyPairs := ca.GetTrustedTLSKeyPairs()
|
||||
if len(keyPairs) == 0 {
|
||||
return nil, trace.BadParameter("certificate authority has no TLS certificates")
|
||||
certPool, count, err := CertPoolFromCertAuthorities([]types.CertAuthority{ca})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
certPool := x509.NewCertPool()
|
||||
for _, keyPair := range keyPairs {
|
||||
cert, err := tlsca.ParseCertificatePEM(keyPair.Cert)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
certPool.AddCert(cert)
|
||||
if count == 0 {
|
||||
return nil, trace.BadParameter("certificate authority has no TLS certificates")
|
||||
}
|
||||
return certPool, nil
|
||||
}
|
||||
|
||||
@@ -208,7 +208,8 @@ type ConnectionsHandler struct {
|
||||
// authMiddleware allows wrapping connections with identity information.
|
||||
authMiddleware *authz.Middleware
|
||||
|
||||
proxyPort string
|
||||
proxyPort string
|
||||
clusterName string
|
||||
|
||||
// getAppByPublicAddress returns a types.Application using the public address as matcher.
|
||||
getAppByPublicAddress func(context.Context, string) (types.Application, error)
|
||||
@@ -245,6 +246,11 @@ func NewConnectionsHandler(closeContext context.Context, cfg *ConnectionsHandler
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
clusterName, err := cfg.AccessPoint.GetClusterName(closeContext)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
c := &ConnectionsHandler{
|
||||
cfg: cfg,
|
||||
closeContext: closeContext,
|
||||
@@ -253,6 +259,7 @@ func NewConnectionsHandler(closeContext context.Context, cfg *ConnectionsHandler
|
||||
gcpHandler: gcpHandler,
|
||||
connAuth: make(map[net.Conn]error),
|
||||
log: slog.With(teleport.ComponentKey, cfg.ServiceComponent),
|
||||
clusterName: clusterName.GetClusterName(),
|
||||
getAppByPublicAddress: func(ctx context.Context, s string) (types.Application, error) {
|
||||
return nil, trace.NotFound("no applications are being proxied")
|
||||
},
|
||||
@@ -270,10 +277,6 @@ func NewConnectionsHandler(closeContext context.Context, cfg *ConnectionsHandler
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
clustername, err := c.cfg.AccessPoint.GetClusterName(closeContext)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Create limiter for connection and rate limiting. Applied to all
|
||||
// app protocols (HTTP, TCP, MCP) in handleConnection.
|
||||
@@ -283,7 +286,7 @@ func NewConnectionsHandler(closeContext context.Context, cfg *ConnectionsHandler
|
||||
}
|
||||
|
||||
// Create and configure HTTP server with authorizing middleware.
|
||||
c.httpServer = c.newHTTPServer(clustername.GetClusterName())
|
||||
c.httpServer = c.newHTTPServer(clusterName.GetClusterName())
|
||||
|
||||
// TCP server will handle TCP applications.
|
||||
tcpServer, err := c.newTCPServer()
|
||||
@@ -421,9 +424,13 @@ func (c *ConnectionsHandler) sessionStartTime(ctx context.Context) time.Time {
|
||||
// newTCPServer creates a server that proxies TCP applications.
|
||||
func (c *ConnectionsHandler) newTCPServer() (*tcpServer, error) {
|
||||
return &tcpServer{
|
||||
emitter: c.cfg.Emitter,
|
||||
hostID: c.cfg.HostID,
|
||||
log: c.log,
|
||||
emitter: c.cfg.Emitter,
|
||||
hostID: c.cfg.HostID,
|
||||
log: c.log,
|
||||
caGetter: c.cfg.AccessPoint,
|
||||
clusterName: c.clusterName,
|
||||
cipherSuites: c.cfg.CipherSuites,
|
||||
insecureMode: c.cfg.InsecureMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,11 @@ type suiteConfig struct {
|
||||
// ManualStart skips calling Start() automatically so the
|
||||
// caller can inject state before starting the server.
|
||||
ManualStart bool
|
||||
// OverrideCAs are cert authorities to upsert into the test cluster after
|
||||
// the auth server starts.
|
||||
OverrideCAs []types.CertAuthority
|
||||
// InsecureMode sets service to insecure mode.
|
||||
InsecureMode bool
|
||||
}
|
||||
|
||||
type fakeConnMonitor struct{}
|
||||
@@ -212,6 +217,10 @@ func SetUpSuiteWithConfig(t *testing.T, config suiteConfig) *Suite {
|
||||
s.tlsServer.Close()
|
||||
})
|
||||
|
||||
for _, ca := range config.OverrideCAs {
|
||||
require.NoError(t, s.tlsServer.Auth().UpsertCertAuthority(s.closeContext, ca))
|
||||
}
|
||||
|
||||
// Set up the host cert pool.
|
||||
rootCA, err := s.tlsServer.Auth().GetCertAuthority(context.Background(), types.CertAuthID{
|
||||
Type: types.HostCA,
|
||||
@@ -380,6 +389,7 @@ func SetUpSuiteWithConfig(t *testing.T, config suiteConfig) *Suite {
|
||||
ConnectionMonitor: fakeConnMonitor{},
|
||||
CipherSuites: utils.DefaultCipherSuites(),
|
||||
ServiceComponent: teleport.ComponentApp,
|
||||
InsecureMode: config.InsecureMode,
|
||||
AWSConfigOptions: []awsconfig.OptionsFn{
|
||||
awsconfig.WithSTSClientProvider(func(_ aws.Config) awsconfig.STSClient {
|
||||
return &mocks.STSClient{}
|
||||
|
||||
+10
-8
@@ -159,14 +159,16 @@ func (c *ConnectionsHandler) withJWTTokenForwarder(ctx context.Context, sess *se
|
||||
// Create a rewriting transport that will be used to forward requests.
|
||||
transport, err := newTransport(c.closeContext,
|
||||
&transportConfig{
|
||||
app: app,
|
||||
publicPort: c.proxyPort,
|
||||
cipherSuites: c.cfg.CipherSuites,
|
||||
jwt: jwt,
|
||||
rewriteTraits: rewriteTraits,
|
||||
log: c.log,
|
||||
hostID: c.cfg.HostID,
|
||||
insecureMode: c.cfg.InsecureMode,
|
||||
app: app,
|
||||
publicPort: c.proxyPort,
|
||||
cipherSuites: c.cfg.CipherSuites,
|
||||
jwt: jwt,
|
||||
rewriteTraits: rewriteTraits,
|
||||
log: c.log,
|
||||
hostID: c.cfg.HostID,
|
||||
insecureMode: c.cfg.InsecureMode,
|
||||
clusterName: c.clusterName,
|
||||
certAuthorityGetter: c.cfg.AccessPoint,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
+76
-10
@@ -20,9 +20,11 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
@@ -31,25 +33,33 @@ import (
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/srv/app/common"
|
||||
"github.com/gravitational/teleport/lib/srv/app/upstreamtls"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
type certificateAuthorityGetter interface {
|
||||
// GetCertAuthority returns cert authority by id.
|
||||
GetCertAuthority(context.Context, apitypes.CertAuthID, bool) (apitypes.CertAuthority, error)
|
||||
}
|
||||
|
||||
type tcpServer struct {
|
||||
emitter apievents.Emitter
|
||||
hostID string
|
||||
log *slog.Logger
|
||||
emitter apievents.Emitter
|
||||
hostID string
|
||||
log *slog.Logger
|
||||
caGetter certificateAuthorityGetter
|
||||
clusterName string
|
||||
cipherSuites []uint16
|
||||
insecureMode bool
|
||||
}
|
||||
|
||||
// handleConnection handles connection from a TCP application.
|
||||
func (s *tcpServer) handleConnection(ctx context.Context, clientConn net.Conn, identity *tlsca.Identity, app apitypes.Application) error {
|
||||
uriAddr, err := utils.ParseAddr(app.GetURI())
|
||||
// Validates the app is TCP.
|
||||
uriAddr, isTLS, err := tcpAppAddr(app)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if uriAddr.AddrNetwork != "tcp" {
|
||||
return trace.BadParameter(`unexpected app %q address network, expected "tcp": %+v`, app.GetName(), uriAddr)
|
||||
}
|
||||
|
||||
dialer := net.Dialer{
|
||||
Timeout: apidefaults.DefaultIOTimeout,
|
||||
@@ -58,9 +68,35 @@ func (s *tcpServer) handleConnection(ctx context.Context, clientConn net.Conn, i
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
serverConn, err := dialer.DialContext(ctx, uriAddr.AddrNetwork, dialTarget)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
var serverConn net.Conn
|
||||
switch {
|
||||
case isTLS:
|
||||
tlsConfig, err := upstreamtls.Configure(ctx, upstreamtls.Options{
|
||||
Logger: s.log,
|
||||
CAGetter: s.caGetter,
|
||||
ClusterName: s.clusterName,
|
||||
App: app,
|
||||
CipherSuites: s.cipherSuites,
|
||||
InsecureMode: s.insecureMode,
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
tlsDialer := tls.Dialer{
|
||||
NetDialer: &dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
serverConn, err = tlsDialer.DialContext(ctx, uriAddr.AddrNetwork, dialTarget)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
serverConn, err = dialer.DialContext(ctx, uriAddr.AddrNetwork, dialTarget)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
audit, err := common.NewAudit(common.AuditConfig{
|
||||
@@ -145,3 +181,33 @@ func ensureZeroTargetPortOrEqualToPortFromURI(uriAddr *utils.NetAddr, targetPort
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tcpAppAddr this function validates the app is a TCP app, and parses its URI.
|
||||
//
|
||||
// Note, instead of changing the `utils.ParseAddr` (which is widely used across
|
||||
// Teleport) to include an internal scheme/protocol we handle this here,
|
||||
// avoiding introducing any regression.
|
||||
func tcpAppAddr(app apitypes.Application) (*utils.NetAddr, bool, error) {
|
||||
uri := app.GetURI()
|
||||
if !app.IsTCP() {
|
||||
return nil, false, trace.BadParameter(`unexpected app %q address network, expected "tcp" or %q: %s`, app.GetName(), apitypes.SchemeTLS, uri)
|
||||
}
|
||||
|
||||
uriAddr, err := utils.ParseAddr(uri)
|
||||
if err == nil {
|
||||
return uriAddr, false, nil
|
||||
}
|
||||
|
||||
tlsScheme := apitypes.SchemeTLS + "://"
|
||||
if !strings.HasPrefix(uri, tlsScheme) {
|
||||
return nil, false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Replaces the "tls://" scheme with "tcp://" and try parsing again.
|
||||
uriAddr, err = utils.ParseAddr(strings.Replace(uri, tlsScheme, "tcp://", 1))
|
||||
if err != nil {
|
||||
return nil, false, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return uriAddr, true, nil
|
||||
}
|
||||
|
||||
+19
-17
@@ -20,7 +20,6 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -39,6 +38,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/srv/app/common"
|
||||
"github.com/gravitational/teleport/lib/srv/app/upstreamtls"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
@@ -56,8 +56,10 @@ type transportConfig struct {
|
||||
rewriteTraits wrappers.Traits
|
||||
log *slog.Logger
|
||||
// hostID is purely for troubleshooting purposes (put in the error messages)
|
||||
hostID string
|
||||
insecureMode bool
|
||||
hostID string
|
||||
insecureMode bool
|
||||
clusterName string
|
||||
certAuthorityGetter upstreamtls.CertificateAuthorityGetter
|
||||
}
|
||||
|
||||
// Check validates configuration.
|
||||
@@ -74,6 +76,12 @@ func (c *transportConfig) Check() error {
|
||||
if c.log == nil {
|
||||
c.log = slog.With(teleport.ComponentKey, "transport")
|
||||
}
|
||||
if c.clusterName == "" {
|
||||
return trace.BadParameter("cluster name missing")
|
||||
}
|
||||
if c.certAuthorityGetter == nil {
|
||||
return trace.BadParameter("cert authority getter missing")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -110,7 +118,14 @@ func newTransport(ctx context.Context, c *transportConfig) (*transport, error) {
|
||||
|
||||
tr.ResponseHeaderTimeout = responseHeaderTimeout
|
||||
|
||||
tr.TLSClientConfig, err = configureTLS(c)
|
||||
tr.TLSClientConfig, err = upstreamtls.Configure(ctx, upstreamtls.Options{
|
||||
Logger: c.log,
|
||||
CAGetter: c.certAuthorityGetter,
|
||||
ClusterName: c.clusterName,
|
||||
App: c.app,
|
||||
CipherSuites: c.cipherSuites,
|
||||
InsecureMode: c.insecureMode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
@@ -284,19 +299,6 @@ func (t *transport) rewriteRedirect(resp *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureTLS creates and configures a *tls.Config that will be used for
|
||||
// mutual authentication.
|
||||
func configureTLS(c *transportConfig) (*tls.Config, error) {
|
||||
tlsConfig := utils.TLSConfig(c.cipherSuites)
|
||||
|
||||
// Don't verify the server's certificate if Teleport was started with
|
||||
// the --insecure flag, or 'insecure_skip_verify' was specifically requested in
|
||||
// the application config.
|
||||
tlsConfig.InsecureSkipVerify = (c.insecureMode || c.app.GetInsecureSkipVerify())
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// host returns the host from a host:port string.
|
||||
func host(addr string) string {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
@@ -197,9 +198,11 @@ func newTestTransport(t *testing.T, dial func(context.Context, string, string) (
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tr, err := newTransport(t.Context(), &transportConfig{
|
||||
app: app,
|
||||
publicPort: "443",
|
||||
jwt: "test-jwt",
|
||||
app: app,
|
||||
publicPort: "443",
|
||||
jwt: "test-jwt",
|
||||
clusterName: "example",
|
||||
certAuthorityGetter: &emptyCertAuthorityGetter{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tr.tr.(*http.Transport).DialContext = dial
|
||||
@@ -266,3 +269,10 @@ func (noopAudit) OnDynamoDBRequest(context.Context, *common.SessionContext, *htt
|
||||
func (noopAudit) EmitEvent(context.Context, apievents.AuditEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type emptyCertAuthorityGetter struct{}
|
||||
|
||||
// GetCertAuthority returns cert authority by id.
|
||||
func (emptyCertAuthorityGetter) GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error) {
|
||||
return nil, trace.NotFound("certificate authority not found")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package upstreamtls
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"iter"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffetls"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/tlsutils"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// CertificateAuthorityGetter used to retrieve CA public information.
|
||||
type CertificateAuthorityGetter interface {
|
||||
// GetCertAuthority returns cert authority by id.
|
||||
GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error)
|
||||
}
|
||||
|
||||
// Options are the options used to configure the TLS.
|
||||
type Options struct {
|
||||
// Logger is the slog.Logger used by the configurator.
|
||||
Logger *slog.Logger
|
||||
// CAGetter is the interface used to retrieve certificate authorities.
|
||||
CAGetter CertificateAuthorityGetter
|
||||
// ClusterName is the current cluster name.
|
||||
ClusterName string
|
||||
// App is the app being configured.
|
||||
App types.Application
|
||||
// CipherSuites are the TLS cipher suites.
|
||||
CipherSuites []uint16
|
||||
// InsecureMode indicates the service is running in insecure mode.
|
||||
InsecureMode bool
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults validates required fields and sets defaults for optional
|
||||
// ones.
|
||||
func (o *Options) CheckAndSetDefaults() error {
|
||||
if o.CAGetter == nil {
|
||||
return trace.BadParameter("CAGetter is required")
|
||||
}
|
||||
if o.ClusterName == "" {
|
||||
return trace.BadParameter("ClusterName is required")
|
||||
}
|
||||
if o.App == nil {
|
||||
return trace.BadParameter("App is required")
|
||||
}
|
||||
if o.Logger == nil {
|
||||
o.Logger = slog.Default()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configure creates and configures a *tls.Config that will be used to verify
|
||||
// upstream TLS servers. This function assumes a validated AppTLS, meaning it
|
||||
// won't perform validation checks for fields and their contents.
|
||||
//
|
||||
// Note that [types.AppTLS] can be nil since it is not required for supported
|
||||
// protocols. This function must take that into account.
|
||||
func Configure(ctx context.Context, opts Options) (*tls.Config, error) {
|
||||
if err := opts.CheckAndSetDefaults(); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Service-level insecure mode takes precedence over app configuration.
|
||||
if opts.InsecureMode {
|
||||
return configureTLSInsecure(opts.CipherSuites)
|
||||
}
|
||||
|
||||
appTLS := cmp.Or(opts.App.GetTLS(), &types.AppTLS{})
|
||||
logger := opts.Logger.With("app", opts.App.GetName())
|
||||
|
||||
caPool, err := newTLSCertPool(ctx, logger, opts.CAGetter, opts.ClusterName, appTLS.AllowedCas)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
switch opts.App.GetTLSMode() {
|
||||
case types.AppTLSModeVerifyFull:
|
||||
spiffeID, err := spiffeid.FromString(appTLS.ServerSpiffeId)
|
||||
if err != nil {
|
||||
return nil, trace.BadParameter("app contains an invalid tls.server_spiffe_id field: %v", err)
|
||||
}
|
||||
return configureTLSVerifyFull(opts.CipherSuites, caPool, opts.App.GetURI(), spiffeID, appTLS.ServerName)
|
||||
case types.AppTLSModeVerifySpiffeID:
|
||||
spiffeID, err := spiffeid.FromString(appTLS.ServerSpiffeId)
|
||||
if err != nil {
|
||||
return nil, trace.BadParameter("app contains an invalid tls.server_spiffe_id field: %v", err)
|
||||
}
|
||||
return configureTLSSpiffeIDVerify(opts.CipherSuites, caPool, spiffeID)
|
||||
case types.AppTLSModeVerifyServerName:
|
||||
return configureTLSVerifyServerName(opts.CipherSuites, caPool, appTLS.ServerName)
|
||||
case types.AppTLSModeInsecure:
|
||||
return configureTLSInsecure(opts.CipherSuites)
|
||||
case "":
|
||||
// Empty mode is returned by GetTLSMode() for app protocols that don't
|
||||
// support TLS configuration. Return baseline tls.Config so callers
|
||||
// still get configured cipher suites.
|
||||
return utils.TLSConfig(opts.CipherSuites), nil
|
||||
default:
|
||||
// This should be unreachable. In case it does, it is probably caused by
|
||||
// a new TLS mode being added to the config but not here. Ensure this
|
||||
// switch-case handles all valid options.
|
||||
return nil, trace.BadParameter("unsupported TLS mode %q", opts.App.GetTLSMode())
|
||||
}
|
||||
}
|
||||
|
||||
func configureTLSSpiffeIDVerify(cipherSuites []uint16, caPool *x509.CertPool, spiffeID spiffeid.ID) (*tls.Config, error) {
|
||||
tlsConfig := utils.TLSConfig(cipherSuites)
|
||||
tlsConfig.RootCAs = caPool
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.ServerName = ""
|
||||
// Skips server name verification.
|
||||
tlsConfig.VerifyConnection = tlsVerifyPeerCertificateWithSPIFFE(tlsConfig.RootCAs, spiffeID, "")
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func configureTLSVerifyFull(cipherSuites []uint16, caPool *x509.CertPool, appURI string, spiffeID spiffeid.ID, serverName string) (*tls.Config, error) {
|
||||
tlsConfig := utils.TLSConfig(cipherSuites)
|
||||
tlsConfig.RootCAs = caPool
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
|
||||
// Since we're providing a custom VerifyConnection we must ensure that
|
||||
// server name is not empty, otherwise it would just skip the server name
|
||||
// verification.
|
||||
//
|
||||
// To do this we parse the app URI and lookup for the hostname.
|
||||
if serverName == "" {
|
||||
u, err := url.Parse(appURI)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
serverName = u.Hostname()
|
||||
if serverName == "" {
|
||||
return nil, trace.BadParameter("cannot derive server name from app URI %q", appURI)
|
||||
}
|
||||
}
|
||||
|
||||
tlsConfig.ServerName = serverName
|
||||
tlsConfig.VerifyConnection = tlsVerifyPeerCertificateWithSPIFFE(tlsConfig.RootCAs, spiffeID, serverName)
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// configureTLSVerifyServerName setups a TLS config with standard hostname
|
||||
// certificate match verification.
|
||||
func configureTLSVerifyServerName(cipherSuites []uint16, caPool *x509.CertPool, serverName string) (*tls.Config, error) {
|
||||
tlsConfig := utils.TLSConfig(cipherSuites)
|
||||
tlsConfig.RootCAs = caPool
|
||||
// In case the property is empty (default value), the standard library
|
||||
// will pickup the appropriate hostname value.
|
||||
tlsConfig.ServerName = serverName
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func configureTLSInsecure(cipherSuites []uint16) (*tls.Config, error) {
|
||||
tlsConfig := utils.TLSConfig(cipherSuites)
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// tlsVerifyPeerCertificateWithSPIFFE creates a tls.VerifyConnection function
|
||||
// that verifies the certificate chain, SPIFFE ID extension, and server name.
|
||||
func tlsVerifyPeerCertificateWithSPIFFE(roots *x509.CertPool, spiffeID spiffeid.ID, serverName string) func(cs tls.ConnectionState) error {
|
||||
return func(cs tls.ConnectionState) error {
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
// When `serverName` is empty, it means no DNSName verification will
|
||||
// be done.
|
||||
DNSName: serverName,
|
||||
|
||||
Intermediates: nil,
|
||||
}
|
||||
if len(cs.PeerCertificates) == 0 {
|
||||
return trace.BadParameter("no peer certificates presented")
|
||||
}
|
||||
if len(cs.PeerCertificates) > 1 {
|
||||
opts.Intermediates = x509.NewCertPool()
|
||||
for _, cert := range cs.PeerCertificates[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
// Returns error if the certificate doesn't contain URI SAN (SPIFFE ID).
|
||||
upstreamID, err := spiffetls.PeerIDFromConnectionState(cs)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if upstreamID != spiffeID {
|
||||
return trace.BadParameter("spiffe id mismatch. expected %q but got %q", spiffeID, upstreamID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// newTLSCertPool creates a new x509 cert pool using the list of allowed CAs.
|
||||
func newTLSCertPool(ctx context.Context, logger *slog.Logger, getter CertificateAuthorityGetter, clusterName string, cas []string) (*x509.CertPool, error) {
|
||||
// If no options are provided, use the host's root CA (default behavior).
|
||||
// This is mainly to keep backwards compatibility for apps using TLS
|
||||
// connections, and that doesn't configure the CA list.
|
||||
if len(cas) == 0 {
|
||||
logger.DebugContext(ctx, "using system trust store")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
for _, ca := range cas {
|
||||
switch {
|
||||
case slices.Contains(types.AppSupportedInternalCAs(), ca):
|
||||
certs, err := loadCACertificates(ctx, getter, clusterName, ca)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var hasAny bool
|
||||
for cert, err := range certs {
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
hasAny = true
|
||||
caPool.AddCert(cert)
|
||||
}
|
||||
|
||||
if !hasAny {
|
||||
logger.WarnContext(ctx, "CA alias contains non active keys, it won't be effective", "alias", ca)
|
||||
}
|
||||
default:
|
||||
caCert, err := tlsutils.ParseCertificatePEMStrict([]byte(ca))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
caPool.AddCert(caCert)
|
||||
}
|
||||
}
|
||||
|
||||
return caPool, nil
|
||||
}
|
||||
|
||||
// loadCACertificates takes a "CA alias" and resolve to Teleport CA certificates.
|
||||
func loadCACertificates(ctx context.Context, getter CertificateAuthorityGetter, clusterName string, alias types.AppTLSInternalCA) (iter.Seq2[*x509.Certificate, error], error) {
|
||||
var caType types.CertAuthType
|
||||
switch alias {
|
||||
case types.AppTLSInternalCAWorkloadIdentity:
|
||||
caType = types.SPIFFECA
|
||||
default:
|
||||
// This should be unreachable. If it happens there is probably a
|
||||
// mismatch between this switch and the
|
||||
// `types.AppSupportedInternalCAs` function.
|
||||
return nil, trace.BadParameter("unsupported CA %q", alias)
|
||||
}
|
||||
|
||||
ca, err := getter.GetCertAuthority(ctx, types.CertAuthID{
|
||||
Type: caType,
|
||||
DomainName: clusterName,
|
||||
}, false)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return services.GetX509Certs(ca), nil
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package upstreamtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils/log/logtest"
|
||||
)
|
||||
|
||||
func TestNewTLSCertPool(t *testing.T) {
|
||||
t.Parallel()
|
||||
const clusterName = "test-cluster"
|
||||
|
||||
_, inlineCAPEM, err := tlsca.GenerateSelfSignedCA(
|
||||
pkix.Name{Organization: []string{"inline-ca"}}, nil, time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, spiffeCAPEM1, err := tlsca.GenerateSelfSignedCA(
|
||||
pkix.Name{Organization: []string{"spiffe-ca-1"}}, nil, time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, spiffeCAPEM2, err := tlsca.GenerateSelfSignedCA(
|
||||
pkix.Name{Organization: []string{"spiffe-ca-2"}}, nil, time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
spiffeCAID := types.CertAuthID{Type: types.SPIFFECA, DomainName: clusterName}
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
cas []string
|
||||
getter CertificateAuthorityGetter
|
||||
expectedErr require.ErrorAssertionFunc
|
||||
expectedPool require.ValueAssertionFunc
|
||||
}{
|
||||
"empty input returns nil pool": {
|
||||
cas: nil,
|
||||
getter: &fakeCAGetter{},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: require.Nil,
|
||||
},
|
||||
"inline PEM only": {
|
||||
cas: []string{string(inlineCAPEM)},
|
||||
getter: &fakeCAGetter{},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: func(tt require.TestingT, i1 any, i2 ...any) {
|
||||
expected := x509.NewCertPool()
|
||||
require.True(tt, expected.AppendCertsFromPEM(inlineCAPEM), i2...)
|
||||
|
||||
generatedPool, _ := i1.(*x509.CertPool)
|
||||
require.True(tt, generatedPool.Equal(expected), "cert pool contents differ")
|
||||
},
|
||||
},
|
||||
"alias resolves to single key pair": {
|
||||
cas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
getter: &fakeCAGetter{cas: map[types.CertAuthID]types.CertAuthority{
|
||||
spiffeCAID: newSPIFFECertAuthority(t, clusterName, spiffeCAPEM1),
|
||||
}},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: func(tt require.TestingT, i1 any, i2 ...any) {
|
||||
expected := x509.NewCertPool()
|
||||
require.True(tt, expected.AppendCertsFromPEM(spiffeCAPEM1), i2...)
|
||||
|
||||
generatedPool, _ := i1.(*x509.CertPool)
|
||||
require.True(tt, generatedPool.Equal(expected), "cert pool contents differ")
|
||||
},
|
||||
},
|
||||
"alias resolves to multiple key pairs (rotation)": {
|
||||
cas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
getter: &fakeCAGetter{cas: map[types.CertAuthID]types.CertAuthority{
|
||||
spiffeCAID: newSPIFFECertAuthority(t, clusterName, spiffeCAPEM1, spiffeCAPEM2),
|
||||
}},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: func(tt require.TestingT, i1 any, i2 ...any) {
|
||||
expected := x509.NewCertPool()
|
||||
require.True(tt, expected.AppendCertsFromPEM(spiffeCAPEM1))
|
||||
require.True(tt, expected.AppendCertsFromPEM(spiffeCAPEM2))
|
||||
|
||||
generatedPool, _ := i1.(*x509.CertPool)
|
||||
require.True(tt, generatedPool.Equal(expected), "cert pool contents differ")
|
||||
},
|
||||
},
|
||||
"alias resolves but CA has no TLS key pairs": {
|
||||
cas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
getter: &fakeCAGetter{cas: map[types.CertAuthID]types.CertAuthority{
|
||||
spiffeCAID: newSPIFFECertAuthority(t, clusterName),
|
||||
}},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: func(tt require.TestingT, i1 any, i2 ...any) {
|
||||
generatedPool, _ := i1.(*x509.CertPool)
|
||||
require.NotNil(t, generatedPool)
|
||||
require.True(tt, generatedPool.Equal(x509.NewCertPool()), "expected empty cert pool")
|
||||
},
|
||||
},
|
||||
"alias getter error is propagated": {
|
||||
cas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
getter: &fakeCAGetter{err: trace.NotFound("no SPIFFE CA")},
|
||||
expectedErr: require.Error,
|
||||
expectedPool: require.Nil,
|
||||
},
|
||||
"inline PEM and alias combined": {
|
||||
cas: []string{string(inlineCAPEM), types.AppTLSInternalCAWorkloadIdentity},
|
||||
getter: &fakeCAGetter{cas: map[types.CertAuthID]types.CertAuthority{
|
||||
spiffeCAID: newSPIFFECertAuthority(t, clusterName, spiffeCAPEM1),
|
||||
}},
|
||||
expectedErr: require.NoError,
|
||||
expectedPool: func(tt require.TestingT, i1 any, i2 ...any) {
|
||||
expected := x509.NewCertPool()
|
||||
require.True(tt, expected.AppendCertsFromPEM(inlineCAPEM))
|
||||
require.True(tt, expected.AppendCertsFromPEM(spiffeCAPEM1))
|
||||
|
||||
generatedPool, _ := i1.(*x509.CertPool)
|
||||
require.True(tt, generatedPool.Equal(expected), "cert pool contents differ")
|
||||
},
|
||||
},
|
||||
"malformed PEM returns error": {
|
||||
cas: []string{"not a pem"},
|
||||
getter: &fakeCAGetter{},
|
||||
expectedErr: require.Error,
|
||||
expectedPool: require.Nil,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pool, err := newTLSCertPool(context.Background(), logtest.NewLogger(), tc.getter, clusterName, tc.cas)
|
||||
tc.expectedErr(t, err)
|
||||
tc.expectedPool(t, pool)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeCAGetter is a CertificateAuthorityGetter backed by an in-memory map.
|
||||
type fakeCAGetter struct {
|
||||
cas map[types.CertAuthID]types.CertAuthority
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeCAGetter) GetCertAuthority(_ context.Context, id types.CertAuthID, _ bool) (types.CertAuthority, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
ca, ok := f.cas[id]
|
||||
if !ok {
|
||||
return nil, trace.NotFound("ca %v not found", id)
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
// newSPIFFECertAuthority builds a SPIFFE CertAuthority whose ActiveKeys.TLS
|
||||
// holds one entry per provided cert PEM.
|
||||
func newSPIFFECertAuthority(t *testing.T, clusterName string, certPEMs ...[]byte) types.CertAuthority {
|
||||
t.Helper()
|
||||
|
||||
keyPairs := make([]*types.TLSKeyPair, 0, len(certPEMs))
|
||||
for _, certPEM := range certPEMs {
|
||||
keyPairs = append(keyPairs, &types.TLSKeyPair{Cert: certPEM})
|
||||
}
|
||||
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
|
||||
Type: types.SPIFFECA,
|
||||
ClusterName: clusterName,
|
||||
ActiveKeys: types.CAKeySet{TLS: keyPairs},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return ca
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2026 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
"github.com/gravitational/teleport/lib/auth/authcatest"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// TestHandleConnectionTLSUpstream verifies that the app service can establish
|
||||
// a TLS connection with an upstream HTTPS application using the "tls" options.
|
||||
//
|
||||
// Note: We can't test default behavior when TLS is empty because our test
|
||||
// server uses self-signed certificate, and setting `SSL_CERT_FILE` would only
|
||||
// work on Linux.
|
||||
func TestHandleConnectionTLSUpstream(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
tlsOptsFunc func(*tlsUpstreamSetup) *types.AppTLS
|
||||
upstreamOpts []upstreamServerOpt
|
||||
insecureMode bool
|
||||
expectError bool
|
||||
}{
|
||||
"verify-server-name": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyServerName,
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-server-name custom server name": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyServerName,
|
||||
ServerName: "example-server.com",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-server-name mismatch": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyServerName,
|
||||
ServerName: "random-server.com",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-server-name untrusted CA": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyServerName,
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-spiffe-id": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-spiffe-id skips server name verify": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-spiffe-id mismatch": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/random",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-spiffe-id non-SVID cert": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/random",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-spiffe-id untrusted CA": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-spiffe-id workload_identity alias": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
withUpstreamCustomCA(types.SPIFFECA),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-spiffe-id workload_identity alias with cert from another CA": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-spiffe-id workload_identity alias combined with inline CA": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifySpiffeID,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{
|
||||
types.AppTLSInternalCAWorkloadIdentity,
|
||||
string(setup.serverCACertPEM),
|
||||
},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-full": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-full unspecified server name": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-full untrusted CA": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-full server name mismatch": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"random-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-full mismatch server spiffe id": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/random"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-full workload_identity alias": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{types.AppTLSInternalCAWorkloadIdentity},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"example-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
withUpstreamCustomCA(types.SPIFFECA),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
"verify-full both mismatch": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "example-server.com",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"random-server.com"}),
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/random"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
"verify-full server name is set to an IP value": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerName: "127.0.0.1",
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
},
|
||||
"verify-full server name falls back to app URI hostname": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyFull,
|
||||
ServerSpiffeId: "spiffe://mycluster/svc/example",
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamSpiffeID("spiffe://mycluster/svc/example"),
|
||||
},
|
||||
},
|
||||
"service level insecure mode takes precedence": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeVerifyServerName,
|
||||
AllowedCas: []string{string(setup.serverCACertPEM)},
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"random-value.com"}),
|
||||
},
|
||||
insecureMode: true,
|
||||
expectError: false,
|
||||
},
|
||||
"insecure": {
|
||||
tlsOptsFunc: func(setup *tlsUpstreamSetup) *types.AppTLS {
|
||||
return &types.AppTLS{
|
||||
Mode: types.AppTLSModeInsecure,
|
||||
}
|
||||
},
|
||||
upstreamOpts: []upstreamServerOpt{
|
||||
withUpstreamCertSANs([]string{"random-value.com"}),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clock := clockwork.NewRealClock()
|
||||
setup := newTLSUpstreamSetup(t)
|
||||
|
||||
httpsMessage := uuid.New().String()
|
||||
httpsUpstreamAddr := setup.startUpstreamHTTPSServer(
|
||||
t,
|
||||
clock,
|
||||
httpsMessage,
|
||||
tc.upstreamOpts...,
|
||||
)
|
||||
tlsMessage := uuid.New().String()
|
||||
tlsUpstreamAddr := setup.startUpstreamTLSServer(
|
||||
t,
|
||||
clock,
|
||||
tlsMessage,
|
||||
tc.upstreamOpts...,
|
||||
)
|
||||
|
||||
// Since we're configuring the TLS verify function, we must ensure
|
||||
// that different handling (IP and DNS) should work the same way.
|
||||
httpsApps, httpsPublicAddrs := configureAppIPAndLocalhost(t, setup, httpsUpstreamAddr, tc.tlsOptsFunc(setup))
|
||||
tlsApps, tlsPublicAddrs := configureAppIPAndLocalhost(t, setup, tlsUpstreamAddr, tc.tlsOptsFunc(setup))
|
||||
|
||||
s := SetUpSuiteWithConfig(t, suiteConfig{
|
||||
Apps: append(httpsApps, tlsApps...),
|
||||
OverrideCAs: []types.CertAuthority{setup.spiffeCAResource},
|
||||
InsecureMode: tc.insecureMode,
|
||||
})
|
||||
|
||||
t.Run("HTTPS", func(t *testing.T) {
|
||||
for _, publicAddr := range httpsPublicAddrs {
|
||||
t.Run(publicAddr, func(t *testing.T) {
|
||||
cert := s.generateCertificate(t, s.user, publicAddr, "")
|
||||
|
||||
s.checkHTTPResponse(t, cert, func(resp *http.Response) {
|
||||
if tc.expectError {
|
||||
require.Equal(t, http.StatusInternalServerError, resp.StatusCode, "expected HTTPS request to fail")
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, httpsMessage, strings.TrimSpace(string(buf)))
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
for _, publicAddr := range tlsPublicAddrs {
|
||||
t.Run(publicAddr, func(t *testing.T) {
|
||||
serverConn, clientConn := net.Pipe()
|
||||
defer serverConn.Close()
|
||||
defer clientConn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
s.appServer.HandleConnection(clientConn)
|
||||
})
|
||||
|
||||
// This is dialing the app server not the upstream
|
||||
// target. We should expect no errors in this phase.
|
||||
cert := s.generateCertificate(t, s.user, publicAddr, "")
|
||||
tlsConn := tls.Client(serverConn, &tls.Config{
|
||||
RootCAs: s.hostCertPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: constants.APIDomain,
|
||||
Time: s.clock.Now,
|
||||
})
|
||||
t.Cleanup(func() { _ = tlsConn.Close() })
|
||||
err := tlsConn.HandshakeContext(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Upstream errors will appear for clients on the first
|
||||
// write.
|
||||
_, err = tlsConn.Write([]byte("hello"))
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := make([]byte, len(tlsMessage))
|
||||
_, err = tlsConn.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tlsMessage, string(buf))
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tlsUpstreamSetup holds the CAs, and helpers needed to configure an upstream
|
||||
// TLS application for testing.
|
||||
type tlsUpstreamSetup struct {
|
||||
serverCA *tlsca.CertAuthority
|
||||
serverCACertPEM []byte
|
||||
spiffeCA *tlsca.CertAuthority
|
||||
spiffeCAResource types.CertAuthority
|
||||
}
|
||||
|
||||
func newTLSUpstreamSetup(t *testing.T) *tlsUpstreamSetup {
|
||||
t.Helper()
|
||||
|
||||
serverCAKey, serverCACertPEM, err := tlsca.GenerateSelfSignedCA(
|
||||
pkix.Name{Organization: []string{"test-upstream-server-ca"}},
|
||||
nil,
|
||||
1*time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverCA, err := tlsca.FromKeys(serverCACertPEM, serverCAKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Build a SPIFFE CA whose private key is owned by the test.
|
||||
spiffeCAResource, err := authcatest.NewCA(types.SPIFFECA, "root.example.com")
|
||||
require.NoError(t, err)
|
||||
spiffeKP := spiffeCAResource.GetActiveKeys().TLS[0]
|
||||
spiffeCA, err := tlsca.FromKeys(spiffeKP.Cert, spiffeKP.Key)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &tlsUpstreamSetup{
|
||||
serverCA: serverCA,
|
||||
serverCACertPEM: serverCACertPEM,
|
||||
spiffeCA: spiffeCA,
|
||||
spiffeCAResource: spiffeCAResource,
|
||||
}
|
||||
}
|
||||
|
||||
type upstreamConfig struct {
|
||||
sans []string
|
||||
spiffeID string
|
||||
customCA types.CertAuthType
|
||||
}
|
||||
|
||||
type upstreamServerOpt func(c *upstreamConfig)
|
||||
|
||||
func withUpstreamCertSANs(sans []string) upstreamServerOpt {
|
||||
return func(c *upstreamConfig) {
|
||||
c.sans = sans
|
||||
}
|
||||
}
|
||||
|
||||
func withUpstreamSpiffeID(s string) upstreamServerOpt {
|
||||
return func(c *upstreamConfig) {
|
||||
c.spiffeID = s
|
||||
}
|
||||
}
|
||||
|
||||
func withUpstreamCustomCA(caType types.CertAuthType) upstreamServerOpt {
|
||||
return func(c *upstreamConfig) {
|
||||
c.customCA = caType
|
||||
}
|
||||
}
|
||||
|
||||
// startUpstreamHTTPSServer starts an httptest HTTPS server using self-signed
|
||||
// certificate.
|
||||
func (m *tlsUpstreamSetup) startUpstreamHTTPSServer(t *testing.T, clock clockwork.Clock, message string, opts ...upstreamServerOpt) string {
|
||||
t.Helper()
|
||||
|
||||
var cfg upstreamConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
upstream := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
fmt.Fprintln(w, message)
|
||||
}))
|
||||
|
||||
// Disable keep alive so we force new connections every time, forcing
|
||||
// certificate validation.
|
||||
upstream.Config.SetKeepAlivesEnabled(false)
|
||||
upstream.TLS = m.generateUpstreamTLSConfig(t, cfg, clock)
|
||||
upstream.StartTLS()
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
return upstream.URL
|
||||
}
|
||||
|
||||
// startUpstreamTLSServer starts an httptest HTTPS server using self-signed
|
||||
// certificate.
|
||||
func (m *tlsUpstreamSetup) startUpstreamTLSServer(t *testing.T, clock clockwork.Clock, message string, opts ...upstreamServerOpt) string {
|
||||
t.Helper()
|
||||
|
||||
var cfg upstreamConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
ln, err := tls.Listen("tcp", "127.0.0.1:0", m.generateUpstreamTLSConfig(t, cfg, clock))
|
||||
require.NoError(t, err)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
t.Cleanup(func() {
|
||||
_ = ln.Close()
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wg.Go(func() {
|
||||
defer conn.Close()
|
||||
|
||||
buf := make([]byte, 32)
|
||||
for {
|
||||
_, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(conn, message)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return types.SchemeTLS + "://" + ln.Addr().String()
|
||||
}
|
||||
|
||||
// generateUpstreamTLSConfig generates a server certificate signed by the server
|
||||
// CA (using real time for NotAfter so the app service transport can validate it).
|
||||
func (m *tlsUpstreamSetup) generateUpstreamTLSConfig(t *testing.T, cfg upstreamConfig, clock clockwork.Clock) *tls.Config {
|
||||
t.Helper()
|
||||
|
||||
serverKey, err := cryptosuites.GenerateKeyWithAlgorithm(cryptosuites.ECDSAP256)
|
||||
require.NoError(t, err)
|
||||
serverKeyPEM, err := keys.MarshalPrivateKey(serverKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
csr := tlsca.CertificateRequest{
|
||||
Clock: clock,
|
||||
PublicKey: serverKey.Public(),
|
||||
Subject: pkix.Name{CommonName: "127.0.0.1"},
|
||||
NotAfter: time.Now().Add(1 * time.Hour),
|
||||
DNSNames: []string{"127.0.0.1", "localhost"},
|
||||
}
|
||||
if len(cfg.sans) > 0 {
|
||||
csr.DNSNames = cfg.sans
|
||||
}
|
||||
|
||||
signingCA := m.serverCA
|
||||
if cfg.customCA == types.SPIFFECA {
|
||||
signingCA = m.spiffeCA
|
||||
}
|
||||
|
||||
var serverCertPEM []byte
|
||||
if cfg.spiffeID != "" {
|
||||
spiffeID, err := spiffeid.FromString(cfg.spiffeID)
|
||||
require.NoError(t, err)
|
||||
serverCertPEM = generateSVIDCertificate(t, *signingCA, csr, spiffeID)
|
||||
} else {
|
||||
serverCertPEM, err = signingCA.GenerateCertificate(csr)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
serverCert, err := tls.X509KeyPair(serverCertPEM, serverKeyPEM)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Disable keep alive so we force new connections every time, forcing
|
||||
// certificate validation.
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{serverCert},
|
||||
Time: clock.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// newApp creates a types.AppV3 configured with TLS options.
|
||||
func (m *tlsUpstreamSetup) newApp(t *testing.T, upstreamURL string, publicAddr string, appTLS *types.AppTLS) *types.AppV3 {
|
||||
t.Helper()
|
||||
|
||||
name := uuid.New().String()
|
||||
app, err := types.NewAppV3(types.Metadata{
|
||||
Name: name,
|
||||
Labels: staticLabels,
|
||||
}, types.AppSpecV3{
|
||||
URI: upstreamURL,
|
||||
PublicAddr: publicAddr,
|
||||
TLS: appTLS,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return app
|
||||
}
|
||||
|
||||
func generateSVIDCertificate(t *testing.T, ca tlsca.CertAuthority, req tlsca.CertificateRequest, spiffeID spiffeid.ID) []byte {
|
||||
require.NoError(t, req.CheckAndSetDefaults())
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Based on Workload idenity issuance service.
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: req.Subject,
|
||||
NotBefore: req.Clock.Now().UTC().Add(-1 * time.Minute),
|
||||
NotAfter: req.NotAfter,
|
||||
// SPEC(X509-SVID) 4.3. Key Usage:
|
||||
// - Leaf SVIDs MUST NOT set keyCertSign or cRLSign.
|
||||
// - Leaf SVIDs MUST set digitalSignature
|
||||
// - They MAY set keyEncipherment and/or keyAgreement;
|
||||
KeyUsage: x509.KeyUsageDigitalSignature |
|
||||
x509.KeyUsageKeyEncipherment |
|
||||
x509.KeyUsageKeyAgreement,
|
||||
// SPEC(X509-SVID) 4.4. Extended Key Usage:
|
||||
// - Leaf SVIDs SHOULD include this extension, and it MAY be marked as critical.
|
||||
// - When included, fields id-kp-serverAuth and id-kp-clientAuth MUST be set.
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
// SPEC(X509-SVID) 4.1. Basic Constraints:
|
||||
// - leaf certificates MUST set the cA field to false
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
// SPEC(X509-SVID) 2. SPIFFE ID:
|
||||
// - The corresponding SPIFFE ID is set as a URI type in the Subject Alternative Name extension
|
||||
// - An X.509 SVID MUST contain exactly one URI SAN, and by extension, exactly one SPIFFE ID.
|
||||
// - An X.509 SVID MAY contain any number of other SAN field types, including DNS SANs.
|
||||
URIs: []*url.URL{spiffeID.URL()},
|
||||
}
|
||||
|
||||
for i := range req.DNSNames {
|
||||
if ip := net.ParseIP(req.DNSNames[i]); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, req.DNSNames[i])
|
||||
}
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, template, ca.Cert, req.PublicKey, ca.Signer)
|
||||
require.NoError(t, err)
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
|
||||
}
|
||||
|
||||
func configureAppIPAndLocalhost(t *testing.T, setup *tlsUpstreamSetup, url string, tlsOptions *types.AppTLS) ([]types.Application, []string) {
|
||||
randomHex, err := utils.CryptoRandomHex(5)
|
||||
require.NoError(t, err)
|
||||
ipPublicAddr := "ip." + randomHex + ".example.com"
|
||||
localhostPublicAddr := "localhost." + randomHex + ".example.com"
|
||||
|
||||
appUsingIP := setup.newApp(t, url, ipPublicAddr, tlsOptions)
|
||||
appUsingLocalhost := setup.newApp(t, convertToLocalhost(t, url), localhostPublicAddr, tlsOptions)
|
||||
|
||||
return []types.Application{appUsingIP, appUsingLocalhost}, []string{ipPublicAddr, localhostPublicAddr}
|
||||
}
|
||||
|
||||
// convertToLocalhost converts a upstream IP address to localhost.
|
||||
func convertToLocalhost(t *testing.T, rawURL string) string {
|
||||
t.Helper()
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
require.NoError(t, err)
|
||||
_, port, err := net.SplitHostPort(u.Host)
|
||||
require.NoError(t, err)
|
||||
u.Host = net.JoinHostPort("localhost", port)
|
||||
return u.String()
|
||||
}
|
||||
@@ -276,6 +276,9 @@ func (f fakeAccessPoint) GetClusterName(context.Context) (types.ClusterName, err
|
||||
})
|
||||
return clusterName, trace.Wrap(err)
|
||||
}
|
||||
func (f fakeAccessPoint) GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error) {
|
||||
return nil, trace.NotImplemented("not implemented")
|
||||
}
|
||||
|
||||
func checkParamsHaveNameField(t *testing.T, params *apievents.Struct, wantName string) {
|
||||
t.Helper()
|
||||
|
||||
+3
-3
@@ -76,7 +76,7 @@ func (s *Server) handleStreamableHTTP(ctx context.Context, sessionCtx *SessionCt
|
||||
}
|
||||
defer session.sessionAuditor.flush(s.cfg.ParentContext)
|
||||
|
||||
transport, err := s.makeStreamableHTTPTransport(session)
|
||||
transport, err := s.makeStreamableHTTPTransport(ctx, session)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "setting up streamable http transport")
|
||||
}
|
||||
@@ -108,14 +108,14 @@ func (s *Server) handleStreamableHTTP(ctx context.Context, sessionCtx *SessionCt
|
||||
return trace.Wrap(s.serveHTTPConn(ctx, sessionCtx.ClientConn, reverseProxy))
|
||||
}
|
||||
|
||||
func (s *Server) makeStreamableHTTPTransport(session *sessionHandler) (http.RoundTripper, error) {
|
||||
func (s *Server) makeStreamableHTTPTransport(ctx context.Context, session *sessionHandler) (http.RoundTripper, error) {
|
||||
targetURI, err := url.Parse(session.App.GetURI())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
targetURI.Scheme = strings.TrimPrefix(targetURI.Scheme, "mcp+")
|
||||
|
||||
targetTransport, err := s.makeBasicHTTPTransport(session.App)
|
||||
targetTransport, err := s.makeBasicHTTPTransport(ctx, session.App)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ import (
|
||||
type AccessPoint interface {
|
||||
services.AuthPreferenceGetter
|
||||
services.ClusterNameGetter
|
||||
|
||||
// GetCertAuthority returns cert authority by id.
|
||||
GetCertAuthority(context.Context, types.CertAuthID, bool) (types.CertAuthority, error)
|
||||
}
|
||||
|
||||
// AuthClient defines functions that the MCP server requires from the auth
|
||||
|
||||
+26
-7
@@ -29,7 +29,7 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/srv/app/upstreamtls"
|
||||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||||
"github.com/gravitational/teleport/lib/utils/mcputils"
|
||||
)
|
||||
@@ -55,7 +55,7 @@ func (s *Server) handleStdioToSSE(ctx context.Context, sessionCtx *SessionCtx) e
|
||||
defer session.close()
|
||||
|
||||
// Use custom transport that adds extra headers including JWT.
|
||||
httpTransport, err := s.makeSSEHTTPTransport(session)
|
||||
httpTransport, err := s.makeSSEHTTPTransport(ctx, session)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "creating HTTP transport")
|
||||
}
|
||||
@@ -124,18 +124,37 @@ func makeSSEBaseURI(app types.Application) (*url.URL, error) {
|
||||
return baseURL, nil
|
||||
}
|
||||
|
||||
func (s *Server) makeBasicHTTPTransport(app types.Application) (http.RoundTripper, error) {
|
||||
func (s *Server) makeBasicHTTPTransport(ctx context.Context, app types.Application) (http.RoundTripper, error) {
|
||||
// Use similar settings from lib/srv/app/transport.go.
|
||||
tr, err := defaults.Transport()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
clusterName, err := s.cfg.AccessPoint.GetClusterName(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// Add a timeout to control how long it takes to (start) getting a response
|
||||
// from the target server.
|
||||
tr.ResponseHeaderTimeout = time.Minute
|
||||
tr.TLSClientConfig = utils.TLSConfig(s.cfg.CipherSuites)
|
||||
tr.TLSClientConfig.InsecureSkipVerify = s.cfg.InsecureMode || app.GetInsecureSkipVerify()
|
||||
|
||||
// Use app TLS options.
|
||||
//
|
||||
// Note: For non-TLS apps (like `mcp+http`) this won't affect the
|
||||
// connections as the transport won't make used of it.
|
||||
tr.TLSClientConfig, err = upstreamtls.Configure(ctx, upstreamtls.Options{
|
||||
Logger: s.cfg.Log,
|
||||
CAGetter: s.cfg.AccessPoint,
|
||||
ClusterName: clusterName.GetClusterName(),
|
||||
App: app,
|
||||
CipherSuites: s.cfg.CipherSuites,
|
||||
InsecureMode: s.cfg.InsecureMode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
@@ -151,8 +170,8 @@ func (t *sseHTTPTransport) RoundTrip(r *http.Request) (resp *http.Response, err
|
||||
return t.targetTransport.RoundTrip(r)
|
||||
}
|
||||
|
||||
func (s *Server) makeSSEHTTPTransport(session *sessionHandler) (http.RoundTripper, error) {
|
||||
targetTransport, err := s.makeBasicHTTPTransport(session.App)
|
||||
func (s *Server) makeSSEHTTPTransport(ctx context.Context, session *sessionHandler) (http.RoundTripper, error) {
|
||||
targetTransport, err := s.makeBasicHTTPTransport(ctx, session.App)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user