diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index cf74386f4..389eb2832 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -72,6 +72,29 @@ func ParseConfigFile(path string) (*Config, error) { return nil, fmt.Errorf("failed to decode HCL file %s: %w", path, err) } + // Re-parse the file to extract the multiple Vault configurations, which we + // need to parse by hand because we don't have a label on the block + root, err := hcl.Parse(buf.String()) + if err != nil { + return nil, fmt.Errorf("failed to parse HCL file %s: %w", path, err) + } + list, ok := root.Node.(*ast.ObjectList) + if !ok { + return nil, fmt.Errorf("error parsing: root should be an object") + } + matches := list.Filter("vault") + if len(matches.Items) > 0 { + if err := parseVaults(c, matches); err != nil { + return nil, fmt.Errorf("error parsing 'vault': %w", err) + } + } + matches = list.Filter("consul") + if len(matches.Items) > 0 { + if err := parseConsuls(c, matches); err != nil { + return nil, fmt.Errorf("error parsing 'consul': %w", err) + } + } + // convert strings to time.Durations tds := []durationConversionMap{ {"gc_interval", &c.Client.GCInterval, &c.Client.GCIntervalHCL, nil}, @@ -152,6 +175,30 @@ func ParseConfigFile(path string) (*Config, error) { }, } + // Parse durations for Consul and Vault config blocks if provided. + // + // Since the map of multiple cluster configuration contains a pointer to + // the default block we don't need to parse it directly. + for name, consulConfig := range c.Consuls { + if consulConfig.ServiceIdentity != nil { + tds = append(tds, durationConversionMap{ + fmt.Sprintf("consuls.%s.service_identity.ttl", name), nil, &consulConfig.ServiceIdentity.TTLHCL, + func(d *time.Duration) { + consulConfig.ServiceIdentity.TTL = d + }, + }) + } + + if consulConfig.TemplateIdentity != nil { + tds = append(tds, durationConversionMap{ + fmt.Sprintf("consuls.%s.template_identity.ttl", name), nil, &consulConfig.TemplateIdentity.TTLHCL, + func(d *time.Duration) { + consulConfig.TemplateIdentity.TTL = d + }, + }) + } + } + // Add enterprise audit sinks for time.Duration parsing for i, sink := range c.Audit.Sinks { tds = append(tds, durationConversionMap{ @@ -164,28 +211,6 @@ func ParseConfigFile(path string) (*Config, error) { return nil, err } - // Re-parse the file to extract the multiple Vault configurations, which we - // need to parse by hand because we don't have a label on the block - root, err := hcl.Parse(buf.String()) - if err != nil { - return nil, fmt.Errorf("failed to parse HCL file %s: %w", path, err) - } - list, ok := root.Node.(*ast.ObjectList) - if !ok { - return nil, fmt.Errorf("error parsing: root should be an object") - } - matches := list.Filter("vault") - if len(matches.Items) > 0 { - if err := parseVaults(c, matches); err != nil { - return nil, fmt.Errorf("error parsing 'vault': %w", err) - } - } - matches = list.Filter("consul") - if len(matches.Items) > 0 { - if err := parseConsuls(c, matches); err != nil { - return nil, fmt.Errorf("error parsing 'consul': %w", err) - } - } // report unexpected keys err = extraKeys(c) if err != nil { diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go index 0fdd05919..5ffdd9568 100644 --- a/command/agent/config_parse_test.go +++ b/command/agent/config_parse_test.go @@ -240,11 +240,15 @@ var basicConfig = &Config{ Audience: []string{"consul.io", "nomad.dev"}, Env: pointer.Of(false), File: pointer.Of(true), + TTL: pointer.Of(1 * time.Hour), + TTLHCL: "1h", }, TemplateIdentity: &config.WorkloadIdentityConfig{ Audience: []string{"consul.io"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(2 * time.Hour), + TTLHCL: "2h", }, }, Consuls: map[string]*config.ConsulConfig{ @@ -276,11 +280,15 @@ var basicConfig = &Config{ Audience: []string{"consul.io", "nomad.dev"}, Env: pointer.Of(false), File: pointer.Of(true), + TTL: pointer.Of(1 * time.Hour), + TTLHCL: "1h", }, TemplateIdentity: &config.WorkloadIdentityConfig{ Audience: []string{"consul.io"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(2 * time.Hour), + TTLHCL: "2h", }, }, }, diff --git a/command/agent/testdata/basic.hcl b/command/agent/testdata/basic.hcl index 14c784db5..c1dc20e6a 100644 --- a/command/agent/testdata/basic.hcl +++ b/command/agent/testdata/basic.hcl @@ -248,11 +248,13 @@ consul { aud = ["consul.io", "nomad.dev"] env = false file = true + ttl = "1h" } template_identity { aud = ["consul.io"] env = true file = false + ttl = "2h" } } diff --git a/command/agent/testdata/basic.json b/command/agent/testdata/basic.json index baabdc528..9f05fede2 100644 --- a/command/agent/testdata/basic.json +++ b/command/agent/testdata/basic.json @@ -173,7 +173,8 @@ "nomad.dev" ], "env": false, - "file": true + "file": true, + "ttl": "1h" }, "ssl": true, "template_identity": { @@ -181,7 +182,8 @@ "consul.io" ], "env": true, - "file": false + "file": false, + "ttl": "2h" }, "timeout": "5s", "token": "token1", diff --git a/nomad/config.go b/nomad/config.go index 68a45e67e..116ce70d2 100644 --- a/nomad/config.go +++ b/nomad/config.go @@ -508,6 +508,9 @@ func workloadIdentityFromConfig(widConfig *config.WorkloadIdentityConfig) *struc if widConfig.File != nil { wid.File = *widConfig.File } + if widConfig.TTL != nil { + wid.TTL = *widConfig.TTL + } return wid } diff --git a/nomad/structs/config/consul_test.go b/nomad/structs/config/consul_test.go index 2a9f451da..7f6c666d2 100644 --- a/nomad/structs/config/consul_test.go +++ b/nomad/structs/config/consul_test.go @@ -99,6 +99,7 @@ func TestConsulConfig_Merge(t *testing.T) { Audience: []string{"consul.io", "nomad.dev"}, Env: pointer.Of(false), File: pointer.Of(true), + TTL: pointer.Of(2 * time.Hour), }, ExtraKeysHCL: []string{"b", "2"}, } @@ -134,6 +135,7 @@ func TestConsulConfig_Merge(t *testing.T) { Audience: []string{"consul.io", "nomad.dev"}, Env: pointer.Of(false), File: pointer.Of(true), + TTL: pointer.Of(2 * time.Hour), }, ExtraKeysHCL: []string{"a", "1"}, // not merged } diff --git a/nomad/structs/config/workload_id.go b/nomad/structs/config/workload_id.go index 664209c98..135668c0b 100644 --- a/nomad/structs/config/workload_id.go +++ b/nomad/structs/config/workload_id.go @@ -5,6 +5,7 @@ package config import ( "slices" + "time" "github.com/hashicorp/go-set" "github.com/hashicorp/nomad/helper/pointer" @@ -31,6 +32,11 @@ type WorkloadIdentityConfig struct { // File writes the Workload Identity into the Task's secrets directory // if set. File *bool `mapstructure:"file"` + + // TTL is used to determine the expiration of the credentials created for + // this identity (eg the JWT "exp" claim). + TTL *time.Duration `mapstructure:"-"` + TTLHCL string `mapstructure:"ttl" json:"-"` } func (wi *WorkloadIdentityConfig) Copy() *WorkloadIdentityConfig { @@ -68,6 +74,12 @@ func (wi *WorkloadIdentityConfig) Equal(other *WorkloadIdentityConfig) bool { if !pointer.Eq(wi.File, other.File) { return false } + if !pointer.Eq(wi.TTL, other.TTL) { + return false + } + if wi.TTLHCL != other.TTLHCL { + return false + } return true } @@ -94,6 +106,10 @@ func (wi *WorkloadIdentityConfig) Merge(other *WorkloadIdentityConfig) *Workload result.Env = pointer.Merge(result.Env, other.Env) result.File = pointer.Merge(result.File, other.File) + result.TTL = pointer.Merge(result.TTL, other.TTL) + if other.TTLHCL != "" { + result.TTLHCL = other.TTLHCL + } return result } diff --git a/nomad/structs/config/workload_id_test.go b/nomad/structs/config/workload_id_test.go index d3cbc12c3..6602fff11 100644 --- a/nomad/structs/config/workload_id_test.go +++ b/nomad/structs/config/workload_id_test.go @@ -5,6 +5,7 @@ package config import ( "testing" + "time" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/pointer" @@ -19,6 +20,7 @@ func TestWorkloadIdentityConfig_Copy(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), } // Verify Copy() returns the same values but different pointer. @@ -31,6 +33,7 @@ func TestWorkloadIdentityConfig_Copy(t *testing.T) { clone.Audience = []string{"aud", "clone"} clone.Env = pointer.Of(false) clone.File = pointer.Of(true) + clone.TTL = pointer.Of(time.Second) must.NotEq(t, original, clone) must.NotEqOp(t, original, clone) @@ -52,12 +55,14 @@ func TestWorkloadIdentityConfig_Equal(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), }, b: &WorkloadIdentityConfig{ Name: "test", Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), }, expectEq: true, }, @@ -121,6 +126,16 @@ func TestWorkloadIdentityConfig_Equal(t *testing.T) { }, expectEq: false, }, + { + name: "different ttl", + a: &WorkloadIdentityConfig{ + TTL: pointer.Of(time.Hour), + }, + b: &WorkloadIdentityConfig{ + TTL: pointer.Of(time.Minute), + }, + expectEq: false, + }, } for _, tc := range testCases { @@ -152,6 +167,7 @@ func TestWorkloadIdentityConfig_Merge(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), }, }, { @@ -164,6 +180,7 @@ func TestWorkloadIdentityConfig_Merge(t *testing.T) { Audience: []string{"aud", "other"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), }, }, { @@ -176,6 +193,7 @@ func TestWorkloadIdentityConfig_Merge(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(false), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), }, }, { @@ -188,6 +206,20 @@ func TestWorkloadIdentityConfig_Merge(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(true), + TTL: pointer.Of(time.Hour), + }, + }, + { + name: "merge ttl", + other: &WorkloadIdentityConfig{ + TTL: pointer.Of(time.Second), + }, + expected: &WorkloadIdentityConfig{ + Name: "test", + Audience: []string{"aud"}, + Env: pointer.Of(true), + File: pointer.Of(false), + TTL: pointer.Of(time.Second), }, }, } @@ -199,6 +231,7 @@ func TestWorkloadIdentityConfig_Merge(t *testing.T) { Audience: []string{"aud"}, Env: pointer.Of(true), File: pointer.Of(false), + TTL: pointer.Of(time.Hour), } got := original.Merge(tc.other) must.Eq(t, tc.expected, got)