diff --git a/cmd/spire-agent/cli/run/run.go b/cmd/spire-agent/cli/run/run.go
index e2522bca27..8b789881f7 100644
--- a/cmd/spire-agent/cli/run/run.go
+++ b/cmd/spire-agent/cli/run/run.go
@@ -39,6 +39,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/log"
 	"github.com/spiffe/spire/pkg/common/pemutil"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 )
 
 const (
@@ -121,6 +122,7 @@ type experimentalConfig struct {
 	NamedPipeName            string `hcl:"named_pipe_name"`
 	AdminNamedPipeName       string `hcl:"admin_named_pipe_name"`
 	UseSyncAuthorizedEntries bool   `hcl:"use_sync_authorized_entries"`
+	RequirePQKEM             bool   `hcl:"require_pq_kem"`
 
 	Flags fflag.RawConfig `hcl:"feature_flags"`
 }
@@ -591,6 +593,12 @@ func NewAgentConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool)
 		ac.AvailabilityTarget = t
 	}
 
+	ac.TLSPolicy = tlspolicy.Policy{
+		RequirePQKEM: c.Agent.Experimental.RequirePQKEM,
+	}
+
+	tlspolicy.LogPolicy(ac.TLSPolicy, log.NewHCLogAdapter(logger, "tlspolicy"))
+
 	if cmp.Diff(experimentalConfig{}, c.Agent.Experimental) != "" {
 		logger.Warn("Experimental features have been enabled. Please see doc/upgrading.md for upgrade and compatibility considerations for experimental features.")
 	}
diff --git a/cmd/spire-agent/cli/run/run_test.go b/cmd/spire-agent/cli/run/run_test.go
index a4938ef2fc..29ab95b732 100644
--- a/cmd/spire-agent/cli/run/run_test.go
+++ b/cmd/spire-agent/cli/run/run_test.go
@@ -615,6 +615,16 @@ func TestMergeInput(t *testing.T) {
 				require.Equal(t, "bar", c.Agent.TrustDomain)
 			},
 		},
+		{
+			msg: "require_pq_kem should be configurable by file",
+			fileInput: func(c *Config) {
+				c.Agent.Experimental.RequirePQKEM = true
+			},
+			cliInput: func(c *agentConfig) {},
+			test: func(t *testing.T, c *Config) {
+				require.True(t, c.Agent.Experimental.RequirePQKEM)
+			},
+		},
 	}
 	cases = append(cases, mergeInputCasesOS()...)
 
@@ -1012,6 +1022,23 @@ func TestNewAgentConfig(t *testing.T) {
 				require.Nil(t, c)
 			},
 		},
+
+		{
+			msg:   "require PQ KEM is disabled (default)",
+			input: func(c *Config) {},
+			test: func(t *testing.T, c *agent.Config) {
+				require.Equal(t, false, c.TLSPolicy.RequirePQKEM)
+			},
+		},
+		{
+			msg: "require PQ KEM is enabled",
+			input: func(c *Config) {
+				c.Agent.Experimental.RequirePQKEM = true
+			},
+			test: func(t *testing.T, c *agent.Config) {
+				require.Equal(t, true, c.TLSPolicy.RequirePQKEM)
+			},
+		},
 	}
 	cases = append(cases, newAgentConfigCasesOS(t)...)
 	for _, testCase := range cases {
diff --git a/cmd/spire-server/cli/run/run.go b/cmd/spire-server/cli/run/run.go
index 97804a41b9..3ba95d9c3d 100644
--- a/cmd/spire-server/cli/run/run.go
+++ b/cmd/spire-server/cli/run/run.go
@@ -36,6 +36,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/health"
 	"github.com/spiffe/spire/pkg/common/log"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/server"
 	"github.com/spiffe/spire/pkg/server/authpolicy"
 	bundleClient "github.com/spiffe/spire/pkg/server/bundle/client"
@@ -109,6 +110,7 @@ type experimentalConfig struct {
 	EventsBasedCache      bool                        `hcl:"events_based_cache"`
 	PruneEventsOlderThan  string                      `hcl:"prune_events_older_than"`
 	SQLTransactionTimeout string                      `hcl:"sql_transaction_timeout"`
+	RequirePQKEM          bool                        `hcl:"require_pq_kem"`
 
 	Flags fflag.RawConfig `hcl:"feature_flags"`
 
@@ -509,6 +511,12 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool
 	sc.ProfilingFreq = c.Server.ProfilingFreq
 	sc.ProfilingNames = c.Server.ProfilingNames
 
+	sc.TLSPolicy = tlspolicy.Policy{
+		RequirePQKEM: c.Server.Experimental.RequirePQKEM,
+	}
+
+	tlspolicy.LogPolicy(sc.TLSPolicy, log.NewHCLogAdapter(logger, "tlspolicy"))
+
 	for _, adminID := range c.Server.AdminIDs {
 		id, err := spiffeid.FromString(adminID)
 		if err != nil {
diff --git a/cmd/spire-server/cli/run/run_test.go b/cmd/spire-server/cli/run/run_test.go
index cb089b1a10..9a9135e14d 100644
--- a/cmd/spire-server/cli/run/run_test.go
+++ b/cmd/spire-server/cli/run/run_test.go
@@ -64,6 +64,7 @@ func TestParseConfigGood(t *testing.T) {
 	_, ok := trustDomainConfig.EndpointProfile.(bundleClient.HTTPSWebProfile)
 	assert.True(t, ok)
 	assert.True(t, c.Server.AuditLogEnabled)
+	assert.True(t, c.Server.Experimental.RequirePQKEM)
 	testParseConfigGoodOS(t, c)
 
 	// Parse/reprint cycle trims outer whitespace
@@ -455,6 +456,16 @@ func TestMergeInput(t *testing.T) {
 				require.True(t, c.Server.AuditLogEnabled)
 			},
 		},
+		{
+			msg: "require_pq_kem should be configurable by file",
+			fileInput: func(c *Config) {
+				c.Server.Experimental.RequirePQKEM = true
+			},
+			cliFlags: []string{},
+			test: func(t *testing.T, c *Config) {
+				require.True(t, c.Server.Experimental.RequirePQKEM)
+			},
+		},
 	}
 	cases = append(cases, mergeInputCasesOS(t)...)
 
@@ -1160,6 +1171,22 @@ func TestNewServerConfig(t *testing.T) {
 				}, c.AdminIDs)
 			},
 		},
+		{
+			msg:   "require PQ KEM is disabled (default)",
+			input: func(c *Config) {},
+			test: func(t *testing.T, c *server.Config) {
+				require.Equal(t, false, c.TLSPolicy.RequirePQKEM)
+			},
+		},
+		{
+			msg: "require PQ KEM is enabled",
+			input: func(c *Config) {
+				c.Server.Experimental.RequirePQKEM = true
+			},
+			test: func(t *testing.T, c *server.Config) {
+				require.Equal(t, true, c.TLSPolicy.RequirePQKEM)
+			},
+		},
 	}
 	cases = append(cases, newServerConfigCasesOS(t)...)
 
diff --git a/doc/plugin_server_upstreamauthority_spire.md b/doc/plugin_server_upstreamauthority_spire.md
index 83bdb6140a..780aa89b35 100644
--- a/doc/plugin_server_upstreamauthority_spire.md
+++ b/doc/plugin_server_upstreamauthority_spire.md
@@ -19,7 +19,8 @@ These are the current experimental configurations:
 
 | experimental                 | Description                                                                                               | Default |
 |------------------------------|-----------------------------------------------------------------------------------------------------------|---------|
-| workload_api_named_pipe_name | Pipe name of the Workload API named pipe (Windows only; e.g. pipe name of the SPIRE Agent API named pipe) |
+| workload_api_named_pipe_name | Pipe name of the Workload API named pipe (Windows only; e.g. pipe name of the SPIRE Agent API named pipe) |         |
+| require_pq_kem               | Require use of a post-quantum-safe key exchange method for TLS handshakes                                 | false   |
 
 Sample configuration (Unix):
 
diff --git a/doc/spire_agent.md b/doc/spire_agent.md
index b747d99b83..456bc44cce 100644
--- a/doc/spire_agent.md
+++ b/doc/spire_agent.md
@@ -78,6 +78,7 @@ This may be useful for templating configuration files, for example across differ
 | `named_pipe_name`             | Pipe name to bind the SPIRE Agent API named pipe (Windows only)                      | \spire-agent\public\api |
 | `sync_interval`               | Sync interval with SPIRE server with exponential backoff                             | 5 sec                   |
 | `use_sync_authorized_entries` | Use SyncAuthorizedEntries API for periodically synchronization of authorized entries | false                   |
+| `require_pq_kem`              | Require use of a post-quantum-safe key exchange method for TLS handshakes            | false                   |
 
 ### Initial trust bundle configuration
 
diff --git a/doc/spire_server.md b/doc/spire_server.md
index 468583185f..f2651b121b 100644
--- a/doc/spire_server.md
+++ b/doc/spire_server.md
@@ -98,6 +98,7 @@ This may be useful for templating configuration files, for example across differ
 | `sql_transaction_timeout` | Maximum time an SQL transaction could take, used by the events based cache to determine when an event id is unlikely to be used anymore.                                                                               | 24h                                |
 | `auth_opa_policy_engine`  | The [auth opa_policy engine](/doc/authorization_policy_engine.md) used for authorization decisions                                                                                                                     | default SPIRE authorization policy |
 | `named_pipe_name`         | Pipe name of the SPIRE Server API named pipe (Windows only)                                                                                                                                                            | \spire-server\private\api          |
+| `require_pq_kem`         | Require use of a post-quantum-safe key exchange method for TLS handshakes                                                                                                                                               | false                              |
 
 | ratelimit     | Description                                                                                                                                        | Default |
 |:--------------|----------------------------------------------------------------------------------------------------------------------------------------------------|---------|
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index ebe7aa7855..550369201f 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -260,6 +260,7 @@ func (a *Agent) attest(ctx context.Context, sto storage.Storage, cat catalog.Cat
 		Log:               a.c.Log.WithField(telemetry.SubsystemName, telemetry.Attestor),
 		ServerAddress:     a.c.ServerAddress,
 		NodeAttestor:      na,
+		TLSPolicy:         a.c.TLSPolicy,
 	}
 	return node_attestor.New(&config).Attest(ctx)
 }
@@ -284,6 +285,7 @@ func (a *Agent) newManager(ctx context.Context, sto storage.Storage, cat catalog
 		SVIDStoreCache:           cache,
 		NodeAttestor:             na,
 		RotationStrategy:         rotationutil.NewRotationStrategy(a.c.AvailabilityTarget),
+		TLSPolicy:                a.c.TLSPolicy,
 	}
 
 	mgr := manager.New(config)
diff --git a/pkg/agent/attestor/node/node.go b/pkg/agent/attestor/node/node.go
index f31113dc52..c7d0cdca3e 100644
--- a/pkg/agent/attestor/node/node.go
+++ b/pkg/agent/attestor/node/node.go
@@ -25,6 +25,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/telemetry"
 	telemetry_agent "github.com/spiffe/spire/pkg/common/telemetry/agent"
 	telemetry_common "github.com/spiffe/spire/pkg/common/telemetry/common"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/util"
 	"github.com/spiffe/spire/pkg/common/x509util"
 	"github.com/zeebo/errs"
@@ -58,6 +59,7 @@ type Config struct {
 	Log               logrus.FieldLogger
 	ServerAddress     string
 	NodeAttestor      nodeattestor.NodeAttestor
+	TLSPolicy         tlspolicy.Policy
 }
 
 type attestor struct {
@@ -256,6 +258,7 @@ func (a *attestor) serverConn(ctx context.Context, bundle *spiffebundle.Bundle)
 			Address:     a.c.ServerAddress,
 			TrustDomain: a.c.TrustDomain,
 			GetBundle:   bundle.X509Authorities,
+			TLSPolicy:   a.c.TLSPolicy,
 		})
 	}
 
diff --git a/pkg/agent/client/client.go b/pkg/agent/client/client.go
index 1ec3522f58..e4699b0663 100644
--- a/pkg/agent/client/client.go
+++ b/pkg/agent/client/client.go
@@ -20,6 +20,7 @@ import (
 	"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
 	"github.com/spiffe/spire/pkg/common/bundleutil"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/proto/spire/common"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
@@ -92,6 +93,9 @@ type Config struct {
 
 	// RotMtx is used to prevent the creation of new connections during SVID rotations
 	RotMtx *sync.RWMutex
+
+	// TLSPolicy determines the post-quantum-safe policy to apply to all TLS connections.
+	TLSPolicy tlspolicy.Policy
 }
 
 type client struct {
@@ -371,6 +375,7 @@ func (c *client) dial(ctx context.Context) (*grpc.ClientConn, error) {
 			}
 			return agentCert
 		},
+		TLSPolicy:   c.c.TLSPolicy,
 		dialContext: c.dialContext,
 	})
 }
diff --git a/pkg/agent/client/dial.go b/pkg/agent/client/dial.go
index 031572b833..2b6689af28 100644
--- a/pkg/agent/client/dial.go
+++ b/pkg/agent/client/dial.go
@@ -14,6 +14,7 @@ import (
 	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
 	"github.com/spiffe/go-spiffe/v2/svid/x509svid"
 	"github.com/spiffe/spire/pkg/common/idutil"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/x509util"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
@@ -38,6 +39,9 @@ type DialServerConfig struct {
 	// certificate to present to the server during the TLS handshake.
 	GetAgentCertificate func() *tls.Certificate
 
+	// TLSPolicy determines the post-quantum-safe policy to apply to all TLS connections.
+	TLSPolicy tlspolicy.Policy
+
 	// dialContext is an optional constructor for the grpc client connection.
 	dialContext func(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error)
 }
@@ -57,6 +61,11 @@ func DialServer(ctx context.Context, config DialServerConfig) (*grpc.ClientConn,
 		tlsConfig = tlsconfig.MTLSClientConfig(newX509SVIDSource(config.GetAgentCertificate), bundleSource, authorizer)
 	}
 
+	err = tlspolicy.ApplyPolicy(tlsConfig, config.TLSPolicy)
+	if err != nil {
+		return nil, err
+	}
+
 	ctx, cancel := context.WithTimeout(ctx, defaultDialTimeout)
 	defer cancel()
 
diff --git a/pkg/agent/config.go b/pkg/agent/config.go
index f1a28b6249..6d0ba53c77 100644
--- a/pkg/agent/config.go
+++ b/pkg/agent/config.go
@@ -12,6 +12,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/catalog"
 	"github.com/spiffe/spire/pkg/common/health"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 )
 
 type Config struct {
@@ -103,6 +104,9 @@ type Config struct {
 
 	// AvailabilityTarget controls how frequently rotate SVIDs
 	AvailabilityTarget time.Duration
+
+	// TLSPolicy determines the post-quantum-safe TLS policy to apply to all TLS connections.
+	TLSPolicy tlspolicy.Policy
 }
 
 func New(c *Config) *Agent {
diff --git a/pkg/agent/manager/config.go b/pkg/agent/manager/config.go
index b21b43be2b..215b82da49 100644
--- a/pkg/agent/manager/config.go
+++ b/pkg/agent/manager/config.go
@@ -18,6 +18,7 @@ import (
 	"github.com/spiffe/spire/pkg/agent/workloadkey"
 	"github.com/spiffe/spire/pkg/common/rotationutil"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 )
 
 // Config holds a cache manager configuration
@@ -43,6 +44,7 @@ type Config struct {
 	DisableLRUCache          bool
 	NodeAttestor             nodeattestor.NodeAttestor
 	RotationStrategy         *rotationutil.RotationStrategy
+	TLSPolicy                tlspolicy.Policy
 
 	// Clk is the clock the manager will use to get time
 	Clk clock.Clock
@@ -83,6 +85,7 @@ func newManager(c *Config) *manager {
 		NodeAttestor:     c.NodeAttestor,
 		Reattestable:     c.Reattestable,
 		RotationStrategy: c.RotationStrategy,
+		TLSPolicy:        c.TLSPolicy,
 	}
 	svidRotator, client := svid.NewRotator(rotCfg)
 
diff --git a/pkg/agent/svid/rotator.go b/pkg/agent/svid/rotator.go
index a608505f4f..85a1487596 100644
--- a/pkg/agent/svid/rotator.go
+++ b/pkg/agent/svid/rotator.go
@@ -367,6 +367,7 @@ func (r *rotator) serverConn(ctx context.Context, bundle *spiffebundle.Bundle) (
 		Address:     r.c.ServerAddr,
 		TrustDomain: r.c.TrustDomain,
 		GetBundle:   bundle.X509Authorities,
+		TLSPolicy:   r.c.TLSPolicy,
 	})
 }
 
diff --git a/pkg/agent/svid/rotator_config.go b/pkg/agent/svid/rotator_config.go
index 203c194ec0..b38498142e 100644
--- a/pkg/agent/svid/rotator_config.go
+++ b/pkg/agent/svid/rotator_config.go
@@ -17,6 +17,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/backoff"
 	"github.com/spiffe/spire/pkg/common/rotationutil"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 )
 
 const DefaultRotatorInterval = 5 * time.Second
@@ -43,6 +44,9 @@ type RotatorConfig struct {
 	Clk clock.Clock
 
 	RotationStrategy *rotationutil.RotationStrategy
+
+	// TLSPolicy determines the post-quantum-safe policy for TLS connections.
+	TLSPolicy tlspolicy.Policy
 }
 
 func NewRotator(c *RotatorConfig) (Rotator, client.Client) {
@@ -85,6 +89,7 @@ func newRotator(c *RotatorConfig) (*rotator, client.Client) {
 			}
 			return s.SVID, s.Key, rootCAs
 		},
+		TLSPolicy: c.TLSPolicy,
 	}
 	client := client.New(cfg)
 
diff --git a/pkg/common/tlspolicy/tlspolicy.go b/pkg/common/tlspolicy/tlspolicy.go
new file mode 100644
index 0000000000..bfbf382ad4
--- /dev/null
+++ b/pkg/common/tlspolicy/tlspolicy.go
@@ -0,0 +1,49 @@
+// Package tlspolicy provides for configuration and enforcement of policies
+// relating to TLS.
+package tlspolicy
+
+import (
+	"crypto/tls"
+
+	"github.com/hashicorp/go-hclog"
+)
+
+// Policy describes policy options to be applied to a TLS configuration.
+//
+// A zero-initialised Policy provides reasonable defaults.
+type Policy struct {
+	// RequirePQKEM determines if a post-quantum-safe KEM should be required for
+	// TLS connections.
+	RequirePQKEM bool
+}
+
+// Not exported by crypto/tls, so we define it here from the I-D.
+const x25519Kyber768Draft00 tls.CurveID = 0x6399
+
+// LogPolicy logs an informational message reporting the configured policy,
+// aiding administrators to determine what policy options have been
+// successfully enabled.
+func LogPolicy(policy Policy, logger hclog.Logger) {
+	if policy.RequirePQKEM {
+		logger.Debug("Experimental option 'require_pq_kem' is enabled; all TLS connections will require use of a post-quantum safe KEM")
+	}
+}
+
+// ApplyPolicy applies the policy options in policy to a given tls.Config,
+// which is assumed to have already been obtained from the go-spiffe tlsconfig
+// package.
+func ApplyPolicy(config *tls.Config, policy Policy) error {
+	if policy.RequirePQKEM {
+		// List only known PQ-safe KEMs as valid curves.
+		config.CurvePreferences = []tls.CurveID{
+			x25519Kyber768Draft00,
+		}
+
+		// Require TLS 1.3, as all PQ-safe KEMs require it anyway.
+		if config.MinVersion < tls.VersionTLS13 {
+			config.MinVersion = tls.VersionTLS13
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/common/tlspolicy/tlspolicy_test.go b/pkg/common/tlspolicy/tlspolicy_test.go
new file mode 100644
index 0000000000..1233e801cd
--- /dev/null
+++ b/pkg/common/tlspolicy/tlspolicy_test.go
@@ -0,0 +1,35 @@
+package tlspolicy
+
+import (
+	"crypto/tls"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestApplyPolicy(t *testing.T) {
+	require := require.New(t)
+
+	tlsConfig := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
+	err := ApplyPolicy(tlsConfig, Policy{})
+	require.NoError(err)
+
+	require.Equal(0, len(tlsConfig.CurvePreferences))
+	require.Equal(uint16(tls.VersionTLS12), tlsConfig.MinVersion)
+
+	tlsConfig = &tls.Config{
+		MinVersion: tls.VersionTLS12,
+		CurvePreferences: []tls.CurveID{
+			x25519Kyber768Draft00, tls.CurveP256,
+		},
+	}
+	err = ApplyPolicy(tlsConfig, Policy{
+		RequirePQKEM: true,
+	})
+	require.NoError(err)
+
+	require.Equal(tlsConfig.CurvePreferences, []tls.CurveID{x25519Kyber768Draft00})
+	require.Equal(tlsConfig.MinVersion, uint16(tls.VersionTLS13))
+}
diff --git a/pkg/server/bundle/client/client.go b/pkg/server/bundle/client/client.go
index 8b1adbd35f..2462a0917b 100644
--- a/pkg/server/bundle/client/client.go
+++ b/pkg/server/bundle/client/client.go
@@ -13,6 +13,7 @@ import (
 	"github.com/spiffe/go-spiffe/v2/spiffeid"
 	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
 	"github.com/spiffe/spire/pkg/common/bundleutil"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/zeebo/errs"
 )
 
@@ -38,6 +39,10 @@ type ClientConfig struct { //revive:disable-line:exported name stutter is intent
 	// is authenticated via Web PKI.
 	SPIFFEAuth *SPIFFEAuthConfig
 
+	// TLSPolicy specifies the post-quantum-security policy used for TLS
+	// connections.
+	TLSPolicy tlspolicy.Policy
+
 	// mutateTransportHook is a hook to influence the transport used during
 	// tests.
 	mutateTransportHook func(*http.Transport)
@@ -66,6 +71,11 @@ func NewClient(config ClientConfig) (Client, error) {
 		authorizer := tlsconfig.AuthorizeID(endpointID)
 
 		transport.TLSClientConfig = tlsconfig.TLSClientConfig(bundle, authorizer)
+
+		err := tlspolicy.ApplyPolicy(transport.TLSClientConfig, config.TLSPolicy)
+		if err != nil {
+			return nil, err
+		}
 	}
 	if config.mutateTransportHook != nil {
 		config.mutateTransportHook(transport)
diff --git a/pkg/server/config.go b/pkg/server/config.go
index fdbef83671..c25a76208b 100644
--- a/pkg/server/config.go
+++ b/pkg/server/config.go
@@ -10,6 +10,7 @@ import (
 	common "github.com/spiffe/spire/pkg/common/catalog"
 	"github.com/spiffe/spire/pkg/common/health"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	loggerv1 "github.com/spiffe/spire/pkg/server/api/logger/v1"
 	"github.com/spiffe/spire/pkg/server/authpolicy"
 	bundle_client "github.com/spiffe/spire/pkg/server/bundle/client"
@@ -120,6 +121,9 @@ type Config struct {
 	// calculation (prefer the TTL passed by the downstream caller, then fall
 	// back to the default X509 CA TTL).
 	UseLegacyDownstreamX509CATTL bool
+
+	// TLSPolicy determines the policy settings to apply to all TLS connections.
+	TLSPolicy tlspolicy.Policy
 }
 
 type ExperimentalConfig struct {
diff --git a/pkg/server/credtemplate/builder.go b/pkg/server/credtemplate/builder.go
index 150d2dd9e2..fb2a4e1ae4 100644
--- a/pkg/server/credtemplate/builder.go
+++ b/pkg/server/credtemplate/builder.go
@@ -15,6 +15,7 @@ import (
 	"github.com/go-jose/go-jose/v4/jwt"
 	"github.com/spiffe/go-spiffe/v2/spiffeid"
 	"github.com/spiffe/spire/pkg/common/idutil"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/x509util"
 	"github.com/spiffe/spire/pkg/server/api"
 	"github.com/spiffe/spire/pkg/server/plugin/credentialcomposer"
@@ -111,6 +112,7 @@ type Config struct {
 	CredentialComposers          []credentialcomposer.CredentialComposer
 	NewSerialNumber              func() (*big.Int, error)
 	UseLegacyDownstreamX509CATTL bool
+	TLSPolicy                    tlspolicy.Policy
 }
 
 type Builder struct {
diff --git a/pkg/server/endpoints/config.go b/pkg/server/endpoints/config.go
index eaa739f577..9b97328940 100644
--- a/pkg/server/endpoints/config.go
+++ b/pkg/server/endpoints/config.go
@@ -14,6 +14,7 @@ import (
 	"github.com/spiffe/go-spiffe/v2/spiffeid"
 	"github.com/spiffe/spire/pkg/common/bundleutil"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/server/api"
 	agentv1 "github.com/spiffe/spire/pkg/server/api/agent/v1"
 	bundlev1 "github.com/spiffe/spire/pkg/server/api/bundle/v1"
@@ -107,6 +108,10 @@ type Config struct {
 	// calculation (prefer the TTL passed by the downstream caller, then fall
 	// back to the default X509 CA TTL).
 	UseLegacyDownstreamX509CATTL bool
+
+	// TLSPolicy determines the post-quantum-safe policy used for all TLS
+	// connections.
+	TLSPolicy tlspolicy.Policy
 }
 
 func (c *Config) maybeMakeBundleEndpointServer() (Server, func(context.Context) error) {
diff --git a/pkg/server/endpoints/endpoints.go b/pkg/server/endpoints/endpoints.go
index 983f3e94ba..9ddad53723 100644
--- a/pkg/server/endpoints/endpoints.go
+++ b/pkg/server/endpoints/endpoints.go
@@ -32,6 +32,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/auth"
 	"github.com/spiffe/spire/pkg/common/peertracker"
 	"github.com/spiffe/spire/pkg/common/telemetry"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/util"
 	"github.com/spiffe/spire/pkg/server/api"
 	"github.com/spiffe/spire/pkg/server/api/middleware"
@@ -84,6 +85,7 @@ type Endpoints struct {
 	AuditLogEnabled              bool
 	AuthPolicyEngine             *authpolicy.Engine
 	AdminIDs                     []spiffeid.ID
+	TLSPolicy                    tlspolicy.Policy
 }
 
 type APIServers struct {
@@ -176,6 +178,7 @@ func New(ctx context.Context, c Config) (*Endpoints, error) {
 		AuditLogEnabled:              c.AuditLogEnabled,
 		AuthPolicyEngine:             c.AuthPolicyEngine,
 		AdminIDs:                     c.AdminIDs,
+		TLSPolicy:                    c.TLSPolicy,
 	}, nil
 }
 
@@ -359,6 +362,11 @@ func (e *Endpoints) getTLSConfig(ctx context.Context) func(*tls.ClientHelloInfo)
 		spiffeTLSConfig.NextProtos = []string{http2.NextProtoTLS}
 		spiffeTLSConfig.VerifyPeerCertificate = e.serverSpiffeVerificationFunc(bundleSrc)
 
+		err := tlspolicy.ApplyPolicy(spiffeTLSConfig, e.TLSPolicy)
+		if err != nil {
+			return nil, err
+		}
+
 		return spiffeTLSConfig, nil
 	}
 }
diff --git a/pkg/server/endpoints/endpoints_test.go b/pkg/server/endpoints/endpoints_test.go
index 18b220c2b4..aee6ee95fd 100644
--- a/pkg/server/endpoints/endpoints_test.go
+++ b/pkg/server/endpoints/endpoints_test.go
@@ -24,6 +24,7 @@ import (
 	svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1"
 	trustdomainv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/trustdomain/v1"
 	"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/util"
 	"github.com/spiffe/spire/pkg/server/authpolicy"
 	"github.com/spiffe/spire/pkg/server/ca/manager"
@@ -102,6 +103,9 @@ func TestNew(t *testing.T) {
 		RateLimit:        rateLimit,
 		Clock:            clk,
 		AuthPolicyEngine: pe,
+		TLSPolicy: tlspolicy.Policy{
+			RequirePQKEM: true,
+		},
 	})
 	require.NoError(t, err)
 	assert.Equal(t, tcpAddr, endpoints.TCPAddr)
@@ -118,6 +122,7 @@ func TestNew(t *testing.T) {
 	assert.NotNil(t, endpoints.BundleEndpointServer)
 	assert.NotNil(t, endpoints.APIServers.LocalAUthorityServer)
 	assert.NotNil(t, endpoints.EntryFetcherPruneEventsTask)
+	assert.True(t, endpoints.TLSPolicy.RequirePQKEM)
 	assert.Equal(t, cat.GetDataStore(), endpoints.DataStore)
 	assert.Equal(t, log, endpoints.Log)
 	assert.Equal(t, metrics, endpoints.Metrics)
@@ -259,19 +264,27 @@ func TestListenAndServe(t *testing.T) {
 	require.NoError(t, err)
 	defer localConn.Close()
 
-	noauthConn := dialTCP(tlsconfig.TLSClientConfig(ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))
+	noauthConfig := tlsconfig.TLSClientConfig(ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
+	require.NoError(t, tlspolicy.ApplyPolicy(noauthConfig, endpoints.TLSPolicy))
+	noauthConn := dialTCP(noauthConfig)
 	defer noauthConn.Close()
 
-	agentConn := dialTCP(tlsconfig.MTLSClientConfig(agentSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))
+	agentConfig := tlsconfig.MTLSClientConfig(agentSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
+	require.NoError(t, tlspolicy.ApplyPolicy(agentConfig, endpoints.TLSPolicy))
+	agentConn := dialTCP(agentConfig)
 	defer agentConn.Close()
 
-	adminConn := dialTCP(tlsconfig.MTLSClientConfig(adminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))
+	adminConfig := tlsconfig.MTLSClientConfig(adminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
+	require.NoError(t, tlspolicy.ApplyPolicy(adminConfig, endpoints.TLSPolicy))
+	adminConn := dialTCP(adminConfig)
 	defer adminConn.Close()
 
 	downstreamConn := dialTCP(tlsconfig.MTLSClientConfig(downstreamSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))
 	defer downstreamConn.Close()
 
-	federatedAdminConn := dialTCP(tlsconfig.MTLSClientConfig(foreignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))
+	federatedAdminConfig := tlsconfig.MTLSClientConfig(foreignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
+	require.NoError(t, tlspolicy.ApplyPolicy(federatedAdminConfig, endpoints.TLSPolicy))
+	federatedAdminConn := dialTCP(federatedAdminConfig)
 	defer federatedAdminConn.Close()
 
 	t.Run("Bad Client SVID", func(t *testing.T) {
@@ -280,8 +293,12 @@ func TestListenAndServe(t *testing.T) {
 		badSVID := testca.New(t, testTD).CreateX509SVID(agentID)
 		ctx, cancel := context.WithTimeout(ctx, time.Second)
 		defer cancel()
+
+		tlsConfig := tlsconfig.MTLSClientConfig(badSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
+		require.NoError(t, tlspolicy.ApplyPolicy(tlsConfig, endpoints.TLSPolicy))
+
 		badConn, err := grpc.DialContext(ctx, endpoints.TCPAddr.String(), grpc.WithBlock(), grpc.FailOnNonTempDialError(true), //nolint: staticcheck // It is going to be resolved on #5152
-			grpc.WithTransportCredentials(credentials.NewTLS(tlsconfig.MTLSClientConfig(badSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)))),
+			grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
 		)
 		if !assert.Error(t, err, "dialing should have failed") {
 			// close the conn if the dialing unexpectedly succeeded
@@ -337,6 +354,8 @@ func TestListenAndServe(t *testing.T) {
 		unfederatedConfig := tlsconfig.MTLSClientConfig(unfederatedForeignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))
 
 		for _, config := range []*tls.Config{unauthenticatedConfig, unauthorizedConfig, unfederatedConfig} {
+			require.NoError(t, tlspolicy.ApplyPolicy(config, endpoints.TLSPolicy))
+
 			conn, err := grpc.NewClient(endpoints.TCPAddr.String(),
 				grpc.WithTransportCredentials(credentials.NewTLS(config)),
 			)
diff --git a/pkg/server/plugin/upstreamauthority/spire/spire.go b/pkg/server/plugin/upstreamauthority/spire/spire.go
index c955c30fd7..55b7fbc65c 100644
--- a/pkg/server/plugin/upstreamauthority/spire/spire.go
+++ b/pkg/server/plugin/upstreamauthority/spire/spire.go
@@ -20,6 +20,7 @@ import (
 	"github.com/spiffe/spire/pkg/common/coretypes/x509certificate"
 	"github.com/spiffe/spire/pkg/common/idutil"
 	"github.com/spiffe/spire/pkg/common/pluginconf"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
@@ -51,6 +52,7 @@ func buildConfig(coreConfig catalog.CoreConfig, hclText string, status *pluginco
 
 type experimentalConfig struct {
 	WorkloadAPINamedPipeName string `hcl:"workload_api_named_pipe_name" json:"workload_api_named_pipe_name"`
+	RequirePQKEM             bool   `hcl:"require_pq_kem" json:"require_pq_kem"`
 }
 
 func BuiltIn() catalog.BuiltIn {
@@ -119,7 +121,13 @@ func (p *Plugin) Configure(_ context.Context, req *configv1.ConfigureRequest) (*
 		return nil, status.Errorf(codes.Internal, "unable to build server ID: %v", err)
 	}
 
-	p.serverClient = newServerClient(serverID, serverAddr, workloadAPIAddr, p.log)
+	tlsPolicy := tlspolicy.Policy{
+		RequirePQKEM: p.config.Experimental.RequirePQKEM,
+	}
+
+	tlspolicy.LogPolicy(tlsPolicy, p.log)
+
+	p.serverClient = newServerClient(serverID, serverAddr, workloadAPIAddr, p.log, tlsPolicy)
 
 	return &configv1.ConfigureResponse{}, nil
 }
diff --git a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go
index 0ef93c43a9..e827ef9504 100644
--- a/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go
+++ b/pkg/server/plugin/upstreamauthority/spire/spire_server_client.go
@@ -15,6 +15,7 @@ import (
 	bundlev1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/bundle/v1"
 	svidv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/svid/v1"
 	"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
+	"github.com/spiffe/spire/pkg/common/tlspolicy"
 	"github.com/spiffe/spire/pkg/common/util"
 	"github.com/spiffe/spire/pkg/common/x509util"
 	"google.golang.org/grpc"
@@ -24,12 +25,13 @@ import (
 )
 
 // newServerClient creates a new spire-server client
-func newServerClient(serverID spiffeid.ID, serverAddr string, workloadAPIAddr net.Addr, log hclog.Logger) *serverClient {
+func newServerClient(serverID spiffeid.ID, serverAddr string, workloadAPIAddr net.Addr, log hclog.Logger, tlsPolicy tlspolicy.Policy) *serverClient {
 	return &serverClient{
 		serverID:        serverID,
 		serverAddr:      serverAddr,
 		workloadAPIAddr: workloadAPIAddr,
 		log:             &logAdapter{log: log},
+		tlsPolicy:       tlsPolicy,
 	}
 }
 
@@ -39,6 +41,7 @@ type serverClient struct {
 	serverAddr      string
 	workloadAPIAddr net.Addr
 	log             logger.Logger
+	tlsPolicy       tlspolicy.Policy
 
 	mtx    sync.RWMutex
 	source *workloadapi.X509Source
@@ -60,6 +63,12 @@ func (c *serverClient) start(ctx context.Context) error {
 	}
 
 	tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeID(c.serverID))
+	err = tlspolicy.ApplyPolicy(tlsConfig, c.tlsPolicy)
+	if err != nil {
+		source.Close()
+		return status.Errorf(codes.Internal, "error applying TLS policy: %v", err)
+	}
+
 	conn, err := grpc.NewClient(c.serverAddr,
 		grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
 	if err != nil {
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 27db8ca41b..6693e2fd2e 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -306,6 +306,7 @@ func (s *Server) newCredBuilder(cat catalog.Catalog) (*credtemplate.Builder, err
 		JWTIssuer:                    s.config.JWTIssuer,
 		CredentialComposers:          cat.GetCredentialComposers(),
 		UseLegacyDownstreamX509CATTL: s.config.UseLegacyDownstreamX509CATTL,
+		TLSPolicy:                    s.config.TLSPolicy,
 	})
 }
 
diff --git a/test/fixture/config/server_good_posix.conf b/test/fixture/config/server_good_posix.conf
index ae273f4c95..3474bf1eab 100644
--- a/test/fixture/config/server_good_posix.conf
+++ b/test/fixture/config/server_good_posix.conf
@@ -36,6 +36,9 @@ server {
             bundle_endpoint_profile "https_web" {}
         }
     }
+    experimental {
+        require_pq_kem = true
+    }
 }
 
 plugins {
diff --git a/test/fixture/config/server_good_windows.conf b/test/fixture/config/server_good_windows.conf
index 3accbbc1ef..54527582b0 100644
--- a/test/fixture/config/server_good_windows.conf
+++ b/test/fixture/config/server_good_windows.conf
@@ -37,6 +37,7 @@ server {
     }
     experimental {
         named_pipe_name = "\\spire-server\\private\\api-test"
+        require_pq_kem = true
     }
 }