mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
server: Add new server.client_introduction config block. (#26315)
The new configuration block exposes some key options which allow cluster administrators to control certain client introduction behaviours. This change introduces the new block and plumbing, so that it is exposed in the Nomad server for consumption via internal processes.
This commit is contained in:
@@ -656,6 +656,24 @@ func convertServerConfig(agentConfig *Config) (*nomad.Config, error) {
|
||||
runtime.NumCPU())
|
||||
}
|
||||
|
||||
// If the operator has specified a client introduction server config block,
|
||||
// translate this into the internal server configuration object.
|
||||
if agentConfig.Server.ClientIntroduction != nil {
|
||||
if agentConfig.Server.ClientIntroduction.Enforcement != "" {
|
||||
conf.NodeIntroductionConfig.Enforcement = agentConfig.Server.ClientIntroduction.Enforcement
|
||||
}
|
||||
if agentConfig.Server.ClientIntroduction.DefaultIdentityTTL > 0 {
|
||||
conf.NodeIntroductionConfig.DefaultIdentityTTL = agentConfig.Server.ClientIntroduction.DefaultIdentityTTL
|
||||
}
|
||||
if agentConfig.Server.ClientIntroduction.MaxIdentityTTL > 0 {
|
||||
conf.NodeIntroductionConfig.MaxIdentityTTL = agentConfig.Server.ClientIntroduction.MaxIdentityTTL
|
||||
}
|
||||
}
|
||||
|
||||
if err := conf.NodeIntroductionConfig.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid server.client_introduction configuration: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1898,3 +1898,60 @@ func TestAgent_ServerConfig_JobDefaultPriority_Bad(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertServerConfig_clientIntroduction(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputClientIntroduction *ClientIntroduction
|
||||
expectedNodeIntroductionConfig *structs.NodeIntroductionConfig
|
||||
}{
|
||||
{
|
||||
name: "nil client introduction",
|
||||
inputClientIntroduction: nil,
|
||||
expectedNodeIntroductionConfig: &structs.NodeIntroductionConfig{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial override",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "strict",
|
||||
},
|
||||
expectedNodeIntroductionConfig: &structs.NodeIntroductionConfig{
|
||||
Enforcement: "strict",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "partial override",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "strict",
|
||||
DefaultIdentityTTL: 50 * time.Minute,
|
||||
MaxIdentityTTL: 300 * time.Minute,
|
||||
},
|
||||
expectedNodeIntroductionConfig: &structs.NodeIntroductionConfig{
|
||||
Enforcement: "strict",
|
||||
DefaultIdentityTTL: 50 * time.Minute,
|
||||
MaxIdentityTTL: 300 * time.Minute,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
baseConfig := DevConfig(nil)
|
||||
must.NoError(t, baseConfig.normalizeAddrs())
|
||||
baseConfig.Server.ClientIntroduction = tc.inputClientIntroduction
|
||||
|
||||
serverConf, err := convertServerConfig(baseConfig)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, tc.expectedNodeIntroductionConfig, serverConf.NodeIntroductionConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,11 @@ type ServerConfig struct {
|
||||
// by withholding peers until enough servers join.
|
||||
BootstrapExpect int `hcl:"bootstrap_expect"`
|
||||
|
||||
// ClientIntroduction is the configuration block that configures the client
|
||||
// introduction feature. This feature allows servers to validate requests
|
||||
// and perform enforcement actions on client registrations.
|
||||
ClientIntroduction *ClientIntroduction `hcl:"client_introduction"`
|
||||
|
||||
// DataDir is the directory to store our state in
|
||||
DataDir string `hcl:"data_dir"`
|
||||
|
||||
@@ -785,6 +790,7 @@ func (s *ServerConfig) Copy() *ServerConfig {
|
||||
ns.JobDefaultPriority = pointer.Copy(s.JobDefaultPriority)
|
||||
ns.JobMaxPriority = pointer.Copy(s.JobMaxPriority)
|
||||
ns.JobTrackedVersions = pointer.Copy(s.JobTrackedVersions)
|
||||
ns.ClientIntroduction = s.ClientIntroduction.Copy()
|
||||
return &ns
|
||||
}
|
||||
|
||||
@@ -1017,6 +1023,110 @@ func (s *Search) Copy() *Search {
|
||||
return &ns
|
||||
}
|
||||
|
||||
// ClientIntroduction is the server configuration block that configures the
|
||||
// client introduction feature. This feature allows servers to validate requests
|
||||
// and perform enforcement actions on client registrations.
|
||||
type ClientIntroduction struct {
|
||||
|
||||
// Enforcement is the level of enforcement that the server will apply to
|
||||
// client registrations. This can be one of "none", "warn", or "strict"
|
||||
// which is also declared within ClientIntroductionEnforcementValues.
|
||||
Enforcement string `hcl:"enforcement"`
|
||||
|
||||
// DefaultIdentityTTL is the TTL assigned to client introduction identities
|
||||
// that are generated by a caller who did not provide a TTL.
|
||||
DefaultIdentityTTL time.Duration
|
||||
DefaultIdentityTTLHCL string `hcl:"default_identity_ttl" json:"-"`
|
||||
|
||||
// MaxIdentityTTL is the maximum TTL that can be assigned to a client
|
||||
// introduction identity. This is used to validate the TTL provided by
|
||||
// caller and allows operators a method to limit TTL requests.
|
||||
MaxIdentityTTL time.Duration
|
||||
MaxIdentityTTLHCL string `hcl:"max_identity_ttl" json:"-"`
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys within the
|
||||
// configuration block. Without this, unexpected keys will be silently
|
||||
// ignored.
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
}
|
||||
|
||||
// ClientIntroductionEnforcementValues are the valid values for the client
|
||||
// introduction enforcement setting.
|
||||
var ClientIntroductionEnforcementValues = []string{"none", "warn", "strict"}
|
||||
|
||||
// Copy creates a copy of the ClientIntroduction configuration block. All fields
|
||||
// are copied, including the ExtraKeysHCL field which is used by HCL to surface
|
||||
// unexpected keys within the configuration block.
|
||||
func (c *ClientIntroduction) Copy() *ClientIntroduction {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newCI := *c
|
||||
newCI.ExtraKeysHCL = slices.Clone(c.ExtraKeysHCL)
|
||||
|
||||
return &newCI
|
||||
}
|
||||
|
||||
// Merge performs a merge of two ClientIntroduction configuration blocks
|
||||
// overwriting the values in the first block with the values in the block passed
|
||||
// into the function.
|
||||
func (c *ClientIntroduction) Merge(z *ClientIntroduction) *ClientIntroduction {
|
||||
if c == nil {
|
||||
return z
|
||||
}
|
||||
|
||||
result := *c
|
||||
|
||||
if z == nil {
|
||||
return &result
|
||||
}
|
||||
|
||||
if z.Enforcement != "" {
|
||||
result.Enforcement = z.Enforcement
|
||||
}
|
||||
if z.DefaultIdentityTTL > 0 {
|
||||
result.DefaultIdentityTTL = z.DefaultIdentityTTL
|
||||
}
|
||||
if z.MaxIdentityTTL > 0 {
|
||||
result.MaxIdentityTTL = z.MaxIdentityTTL
|
||||
}
|
||||
if len(z.ExtraKeysHCL) > 0 {
|
||||
result.ExtraKeysHCL = append(result.ExtraKeysHCL, z.ExtraKeysHCL...)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Validate performs validation on the ClientIntroduction configuration block to
|
||||
// ensure the values are set correctly for use by the server.
|
||||
func (c *ClientIntroduction) Validate() error {
|
||||
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Enforcement == "" {
|
||||
return errors.New("client_introduction.enforcement must be set")
|
||||
}
|
||||
if !slices.Contains(ClientIntroductionEnforcementValues, c.Enforcement) {
|
||||
return fmt.Errorf("client_introduction.enforcement must be one of %v",
|
||||
ClientIntroductionEnforcementValues)
|
||||
}
|
||||
|
||||
if c.DefaultIdentityTTL < 1 {
|
||||
return errors.New("client_introduction.default_identity_ttl must be greater one")
|
||||
}
|
||||
if c.MaxIdentityTTL < 1 {
|
||||
return errors.New("client_introduction.max_identity_ttl must be greater one")
|
||||
}
|
||||
if c.MaxIdentityTTL < c.DefaultIdentityTTL {
|
||||
return errors.New("client_introduction.max_identity_ttl must be greater than default_identity_ttl")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerJoin is used in both clients and servers to bootstrap connections to
|
||||
// servers
|
||||
type ServerJoin struct {
|
||||
@@ -2456,6 +2566,11 @@ func (s *ServerConfig) Merge(b *ServerConfig) *ServerConfig {
|
||||
result.StartTimeout = b.StartTimeout
|
||||
}
|
||||
|
||||
// Merge the client introduction config.
|
||||
if b.ClientIntroduction != nil {
|
||||
result.ClientIntroduction = result.ClientIntroduction.Merge(b.ClientIntroduction)
|
||||
}
|
||||
|
||||
// Add the schedulers
|
||||
result.EnabledSchedulers = append(result.EnabledSchedulers, b.EnabledSchedulers...)
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ func ParseConfigFile(path string) (*Config, error) {
|
||||
},
|
||||
},
|
||||
Server: &ServerConfig{
|
||||
ClientIntroduction: &ClientIntroduction{},
|
||||
PlanRejectionTracker: &PlanRejectionTracker{},
|
||||
ServerJoin: &ServerJoin{},
|
||||
},
|
||||
@@ -189,6 +190,18 @@ func ParseConfigFile(path string) (*Config, error) {
|
||||
{"rpc.connection_write_timeout", &c.RPC.ConnectionWriteTimeout, &c.RPC.ConnectionWriteTimeoutHCL, nil},
|
||||
{"rpc.stream_open_timeout", &c.RPC.StreamOpenTimeout, &c.RPC.StreamOpenTimeoutHCL, nil},
|
||||
{"rpc.stream_close_timeout", &c.RPC.StreamCloseTimeout, &c.RPC.StreamCloseTimeoutHCL, nil},
|
||||
{
|
||||
"server.client_introduction.default_identity_ttl",
|
||||
&c.Server.ClientIntroduction.DefaultIdentityTTL,
|
||||
&c.Server.ClientIntroduction.DefaultIdentityTTLHCL,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"server.client_introduction.max_identity_ttl",
|
||||
&c.Server.ClientIntroduction.MaxIdentityTTL,
|
||||
&c.Server.ClientIntroduction.MaxIdentityTTLHCL,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
// Parse durations for Consul and Vault config blocks if provided.
|
||||
|
||||
@@ -160,6 +160,13 @@ var basicConfig = &Config{
|
||||
JobDefaultPriority: pointer.Of(100),
|
||||
JobMaxPriority: pointer.Of(200),
|
||||
StartTimeout: "1m",
|
||||
ClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTLHCL: "5m",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTLHCL: "30m",
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
@@ -620,6 +627,9 @@ func (c *Config) addDefaults() {
|
||||
if c.Server.PlanRejectionTracker == nil {
|
||||
c.Server.PlanRejectionTracker = &PlanRejectionTracker{}
|
||||
}
|
||||
if c.Server.ClientIntroduction == nil {
|
||||
c.Server.ClientIntroduction = &ClientIntroduction{}
|
||||
}
|
||||
if c.Reporting == nil {
|
||||
c.Reporting = &config.ReportingConfig{
|
||||
License: &config.LicenseReportingConfig{
|
||||
@@ -712,6 +722,7 @@ var sample0 = &Config{
|
||||
NodeWindow: 31 * time.Minute,
|
||||
NodeWindowHCL: "31m",
|
||||
},
|
||||
ClientIntroduction: &ClientIntroduction{},
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
@@ -821,6 +832,7 @@ var sample1 = &Config{
|
||||
NodeWindow: 31 * time.Minute,
|
||||
NodeWindowHCL: "31m",
|
||||
},
|
||||
ClientIntroduction: &ClientIntroduction{},
|
||||
},
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
|
||||
@@ -1260,6 +1260,123 @@ func TestIsMissingPort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIntroduction_Copy(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
clientIntro := &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
}
|
||||
|
||||
copiedClientIntro := clientIntro.Copy()
|
||||
|
||||
// Ensure the copied object contains the same values, but the underlying
|
||||
// pointer address is different.
|
||||
must.Eq(t, clientIntro, copiedClientIntro)
|
||||
must.NotEq(t, fmt.Sprintf("%p", clientIntro), fmt.Sprintf("%p", copiedClientIntro))
|
||||
}
|
||||
|
||||
func TestClientIntroduction_Merge(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
clientIntro1 := &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
ExtraKeysHCL: []string{"key1", "key2"},
|
||||
}
|
||||
clientIntro2 := &ClientIntroduction{
|
||||
Enforcement: "strict",
|
||||
DefaultIdentityTTL: 30 * time.Minute,
|
||||
MaxIdentityTTL: 60 * time.Minute,
|
||||
ExtraKeysHCL: []string{"key3", "key4"},
|
||||
}
|
||||
expectedClientIntro := &ClientIntroduction{
|
||||
Enforcement: "strict",
|
||||
DefaultIdentityTTL: 30 * time.Minute,
|
||||
MaxIdentityTTL: 60 * time.Minute,
|
||||
ExtraKeysHCL: []string{"key1", "key2", "key3", "key4"},
|
||||
}
|
||||
must.Eq(t, expectedClientIntro, clientIntro1.Merge(clientIntro2))
|
||||
}
|
||||
|
||||
func TestClientIntroduction_Validate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputClientIntroduction *ClientIntroduction
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "nil block",
|
||||
inputClientIntroduction: nil,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "empty enforcement",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "",
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid enforcement",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "nuclear",
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid default_identity_ttl",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 0,
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid max_identity_ttl",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 0,
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid ttl combination",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 4 * time.Minute,
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
inputClientIntroduction: &ClientIntroduction{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualOutput := tc.inputClientIntroduction.Validate()
|
||||
if tc.expectedError {
|
||||
must.Error(t, actualOutput)
|
||||
} else {
|
||||
must.NoError(t, actualOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeServerJoin(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
||||
6
command/agent/testdata/basic.hcl
vendored
6
command/agent/testdata/basic.hcl
vendored
@@ -168,6 +168,12 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
client_introduction {
|
||||
enforcement = "warn"
|
||||
default_identity_ttl = "5m"
|
||||
max_identity_ttl = "30m"
|
||||
}
|
||||
|
||||
license_path = "/tmp/nomad.hclic"
|
||||
}
|
||||
|
||||
|
||||
5
command/agent/testdata/basic.json
vendored
5
command/agent/testdata/basic.json
vendored
@@ -361,6 +361,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"client_introduction": {
|
||||
"enforcement": "warn",
|
||||
"default_identity_ttl": "5m",
|
||||
"max_identity_ttl": "30m"
|
||||
},
|
||||
"upgrade_version": "0.8.0",
|
||||
"license_path": "/tmp/nomad.hclic",
|
||||
"job_default_priority": 100,
|
||||
|
||||
@@ -448,6 +448,11 @@ type Config struct {
|
||||
// considered healthy. Without this, the server can hang indefinitely
|
||||
// waiting for these.
|
||||
StartTimeout time.Duration
|
||||
|
||||
// NodeIntroductionConfig is the configuration for the node introduction
|
||||
// feature. This feature allows servers to validate node registration
|
||||
// requests and perform the appropriate enforcement actions.
|
||||
NodeIntroductionConfig *structs.NodeIntroductionConfig
|
||||
}
|
||||
|
||||
func (c *Config) Copy() *Config {
|
||||
@@ -477,6 +482,7 @@ func (c *Config) Copy() *Config {
|
||||
nc.LicenseConfig = c.LicenseConfig.Copy()
|
||||
nc.SearchConfig = c.SearchConfig.Copy()
|
||||
nc.KEKProviderConfigs = helper.CopySlice(c.KEKProviderConfigs)
|
||||
nc.NodeIntroductionConfig = c.NodeIntroductionConfig.Copy()
|
||||
|
||||
return &nc
|
||||
}
|
||||
@@ -657,6 +663,7 @@ func DefaultConfig() *Config {
|
||||
JobMaxPriority: structs.JobDefaultMaxPriority,
|
||||
JobTrackedVersions: structs.JobDefaultTrackedVersions,
|
||||
StartTimeout: 30 * time.Second,
|
||||
NodeIntroductionConfig: structs.DefaultNodeIntroductionConfig(),
|
||||
}
|
||||
|
||||
// Enable all known schedulers by default
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
)
|
||||
@@ -497,10 +498,10 @@ type NodeMetaResponse struct {
|
||||
|
||||
// NodeIdentityClaims represents the claims for a Nomad node identity JWT.
|
||||
type NodeIdentityClaims struct {
|
||||
NodeID string `json:"nomad_node_id"`
|
||||
NodePool string `json:"nomad_node_pool"`
|
||||
NodeClass string `json:"nomad_node_class"`
|
||||
NodeDatacenter string `json:"nomad_node_datacenter"`
|
||||
NodeID string `json:"nomad_node_id,omitempty"`
|
||||
NodePool string `json:"nomad_node_pool,omitempty"`
|
||||
NodeClass string `json:"nomad_node_class,omitempty"`
|
||||
NodeDatacenter string `json:"nomad_node_datacenter,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateNodeIdentityClaims creates a new NodeIdentityClaims for the given
|
||||
@@ -736,3 +737,98 @@ type NodeIdentityRenewReq struct {
|
||||
}
|
||||
|
||||
type NodeIdentityRenewResp struct{}
|
||||
|
||||
// DefaultNodeIntroductionConfig returns a default and fully hydrated
|
||||
// configuration object for the node introduction feature.
|
||||
func DefaultNodeIntroductionConfig() *NodeIntroductionConfig {
|
||||
return &NodeIntroductionConfig{
|
||||
Enforcement: NodeIntroductionEnforcementWarn,
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// NodeIntroductionEnforcementNone means secure intro token, and the
|
||||
// pre-1.11 workflow that uses an empty authentication token can be used for
|
||||
// initial client registration. No enforcement is applied and no emits are
|
||||
// emitted based on the registration introduction.
|
||||
NodeIntroductionEnforcementNone = "none"
|
||||
|
||||
// NodeIntroductionEnforcementWarn means secure intro token, and the
|
||||
// pre-1.11 workflow that uses an empty authentication token can be used for
|
||||
// initial client registration. The server will emit a log and a metric for
|
||||
// each registration that does not use an introduction token.
|
||||
NodeIntroductionEnforcementWarn = "warn"
|
||||
|
||||
// NodeIntroductionEnforcementStrict means initial client registration must
|
||||
// use a secure introduction token. The server will reject any registration
|
||||
// that does not use an introduction token.
|
||||
NodeIntroductionEnforcementStrict = "strict"
|
||||
)
|
||||
|
||||
// NodeIntroductionConfig is the server configuration block that configures
|
||||
// the client introduction feature. This feature allows servers to validate
|
||||
// requests and perform enforcement actions on client registrations.
|
||||
type NodeIntroductionConfig struct {
|
||||
|
||||
// Enforcement is the level of enforcement that the server will apply to
|
||||
// client registrations. This can be one of "none", "warn", or "strict"
|
||||
// which are defined as NodeIntroductionEnforcementNone,
|
||||
// NodeIntroductionEnforcementWarn, and NodeIntroductionEnforcementStrict
|
||||
// respectively.
|
||||
Enforcement string
|
||||
|
||||
// DefaultIdentityTTL is the TTL assigned to client introduction identities
|
||||
// that are generated by a caller who did not provide a TTL.
|
||||
DefaultIdentityTTL time.Duration
|
||||
|
||||
// MaxIdentityTTL is the maximum TTL that can be assigned to a client
|
||||
// introduction identity. This is used to validate the TTL provided by
|
||||
// caller and allows operators a method to limit TTL requests.
|
||||
MaxIdentityTTL time.Duration
|
||||
}
|
||||
|
||||
// Copy creates a copy of the node introduction configuration block.
|
||||
func (n *NodeIntroductionConfig) Copy() *NodeIntroductionConfig {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newCI := *n
|
||||
return &newCI
|
||||
}
|
||||
|
||||
// Validate checks that the node introduction configuration is valid.
|
||||
func (n *NodeIntroductionConfig) Validate() error {
|
||||
|
||||
if n == nil {
|
||||
return fmt.Errorf("cannot be empty")
|
||||
}
|
||||
|
||||
var mErr *multierror.Error
|
||||
|
||||
switch n.Enforcement {
|
||||
case NodeIntroductionEnforcementNone,
|
||||
NodeIntroductionEnforcementWarn,
|
||||
NodeIntroductionEnforcementStrict:
|
||||
default:
|
||||
mErr = multierror.Append(mErr, fmt.Errorf("invalid enforcement %q", n.Enforcement))
|
||||
}
|
||||
|
||||
if n.DefaultIdentityTTL < 1 {
|
||||
mErr = multierror.Append(mErr, errors.New("default_identity_ttl must be greater than 0"))
|
||||
}
|
||||
|
||||
if n.MaxIdentityTTL < 1 {
|
||||
mErr = multierror.Append(mErr, errors.New("max_identity_ttl must be greater than 0"))
|
||||
}
|
||||
|
||||
if n.MaxIdentityTTL < n.DefaultIdentityTTL {
|
||||
mErr = multierror.Append(mErr, errors.New(
|
||||
"max_identity_ttl must be greater than or equal to default_identity_ttl",
|
||||
))
|
||||
}
|
||||
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -269,10 +270,10 @@ func TestGenerateNodeIdentityClaims(t *testing.T) {
|
||||
|
||||
claims := GenerateNodeIdentityClaims(node, "euw", 10*time.Minute)
|
||||
|
||||
must.Eq(t, "node-id-1", claims.NodeID)
|
||||
must.Eq(t, "custom-pool", claims.NodePool)
|
||||
must.Eq(t, "custom-class", claims.NodeClass)
|
||||
must.Eq(t, "euw2", claims.NodeDatacenter)
|
||||
must.Eq(t, "node-id-1", claims.NodeIdentityClaims.NodeID)
|
||||
must.Eq(t, "custom-pool", claims.NodeIdentityClaims.NodePool)
|
||||
must.Eq(t, "custom-class", claims.NodeIdentityClaims.NodeClass)
|
||||
must.Eq(t, "euw2", claims.NodeIdentityClaims.NodeDatacenter)
|
||||
must.StrEqFold(t, "node:euw:custom-pool:node-id-1:default", claims.Subject)
|
||||
must.Eq(t, []string{IdentityDefaultAud}, claims.Audience)
|
||||
must.NotNil(t, claims.ID)
|
||||
@@ -649,3 +650,103 @@ func TestNodeUpdateStatusRequest_IdentitySigningErrorIsTerminal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DefaultNodeIntroductionConfig(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
expected := &NodeIntroductionConfig{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
}
|
||||
must.Eq(t, expected, DefaultNodeIntroductionConfig())
|
||||
}
|
||||
|
||||
func TestNodeIntroductionConfig_Copy(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
nodeIntro := &NodeIntroductionConfig{
|
||||
Enforcement: "warn",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
}
|
||||
|
||||
copiedNodeIntro := nodeIntro.Copy()
|
||||
|
||||
// Ensure the copied object contains the same values, but the underlying
|
||||
// pointer address is different.
|
||||
must.Eq(t, nodeIntro, copiedNodeIntro)
|
||||
must.NotEq(t, fmt.Sprintf("%p", nodeIntro), fmt.Sprintf("%p", copiedNodeIntro))
|
||||
}
|
||||
|
||||
func TestNodeIntroductionConfig_Validate(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputNodeIntroductionConfig *NodeIntroductionConfig
|
||||
expectedErrorContains string
|
||||
}{
|
||||
{
|
||||
name: "nil config",
|
||||
inputNodeIntroductionConfig: nil,
|
||||
expectedErrorContains: "cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "incorrect enforcement",
|
||||
inputNodeIntroductionConfig: &NodeIntroductionConfig{
|
||||
Enforcement: "invalid",
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
expectedErrorContains: "invalid enforcement",
|
||||
},
|
||||
{
|
||||
name: "incorrect default identity TTL",
|
||||
inputNodeIntroductionConfig: &NodeIntroductionConfig{
|
||||
Enforcement: NodeIntroductionEnforcementStrict,
|
||||
DefaultIdentityTTL: 0,
|
||||
MaxIdentityTTL: 30 * time.Minute,
|
||||
},
|
||||
expectedErrorContains: "default_identity_ttl must be greater than 0",
|
||||
},
|
||||
{
|
||||
name: "incorrect max identity TTL",
|
||||
inputNodeIntroductionConfig: &NodeIntroductionConfig{
|
||||
Enforcement: NodeIntroductionEnforcementStrict,
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 0,
|
||||
},
|
||||
expectedErrorContains: "max_identity_ttl must be greater than 0",
|
||||
},
|
||||
{
|
||||
name: "incorrect max identity TTL greater than default identity TTL",
|
||||
inputNodeIntroductionConfig: &NodeIntroductionConfig{
|
||||
Enforcement: NodeIntroductionEnforcementStrict,
|
||||
DefaultIdentityTTL: 5 * time.Minute,
|
||||
MaxIdentityTTL: 0,
|
||||
},
|
||||
expectedErrorContains: "max_identity_ttl must be greater than or equal to default_identity_ttl",
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
inputNodeIntroductionConfig: &NodeIntroductionConfig{
|
||||
Enforcement: NodeIntroductionEnforcementStrict,
|
||||
DefaultIdentityTTL: 15 * time.Minute,
|
||||
MaxIdentityTTL: 45 * time.Minute,
|
||||
},
|
||||
expectedErrorContains: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualError := tc.inputNodeIntroductionConfig.Validate()
|
||||
if tc.expectedErrorContains == "" {
|
||||
must.NoError(t, actualError)
|
||||
} else {
|
||||
must.ErrorContains(t, actualError, tc.expectedErrorContains)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user