Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/27786.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
identity: allow additional claims to be added to workload identities
```
2 changes: 2 additions & 0 deletions api/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ type Namespace struct {
Meta map[string]string
CreateIndex uint64
ModifyIndex uint64
RequiredExtraClaims map[string]string
OptionalExtraClaims map[string]string
}

// NamespaceCapabilities represents a set of capabilities allowed for this
Expand Down
1 change: 1 addition & 0 deletions api/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ type WorkloadIdentity struct {
Filepath string `hcl:"filepath,optional"`
ServiceName string `hcl:"service_name,optional"`
TTL time.Duration `mapstructure:"ttl" hcl:"ttl,optional"`
ExtraClaims []string `mapstructure:"extra_claims" hcl:"extra_claims,optional"`
}

type Action struct {
Expand Down
2 changes: 1 addition & 1 deletion command/agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1506,7 +1506,7 @@ func TestHTTPServer_ResolveToken(t *testing.T) {
must.NoError(t, srv.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{policy}))
must.NoError(t, srv.State().UpsertAllocs(structs.MsgTypeTestSetup, 100, []*structs.Allocation{alloc}))

claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wih, identity).
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wih, identity, mock.Namespace()).
WithTask(task).Build(time.Now())

testutil.WaitForKeyring(t, srv.RPC, "global")
Expand Down
1 change: 1 addition & 0 deletions command/agent/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,7 @@ func apiWorkloadIdentityToStructs(in *api.WorkloadIdentity) *structs.WorkloadIde
Filepath: in.Filepath,
ServiceName: in.ServiceName,
TTL: in.TTL,
ExtraClaims: slices.Clone(in.ExtraClaims),
}
}

Expand Down
26 changes: 26 additions & 0 deletions command/namespace_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ func parseNamespaceSpecImpl(result *api.Namespace, list *ast.ObjectList) error {
delete(m, "node_pool_config")
delete(m, "vault")
delete(m, "consul")
delete(m, "required_extra_claims")
delete(m, "optional_extra_claims")

// Decode the rest
if err := mapstructure.WeakDecode(m, result); err != nil {
Expand Down Expand Up @@ -311,5 +313,29 @@ func parseNamespaceSpecImpl(result *api.Namespace, list *ast.ObjectList) error {
}
}

if reqClaimsO := list.Filter("required_extra_claims"); len(reqClaimsO.Items) > 0 {
for _, o := range reqClaimsO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return err
}
if err := mapstructure.WeakDecode(m, &result.RequiredExtraClaims); err != nil {
return err
}
}
}

if optClaimsO := list.Filter("optional_extra_claims"); len(optClaimsO.Items) > 0 {
for _, o := range optClaimsO.Elem().Items {
var m map[string]interface{}
if err := hcl.DecodeObject(&m, o.Val); err != nil {
return err
}
if err := mapstructure.WeakDecode(m, &result.OptionalExtraClaims); err != nil {
return err
}
}
}

return nil
}
19 changes: 18 additions & 1 deletion command/namespace_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,17 @@ consul {

meta {
dept = "eng"
}`,
}

required_extra_claims {
foo = "${job.namespace}"
}

optional_extra_claims {
bar = "class:${node.class}"
baz = "dc:${node.datacenter}"
}
`,
expected: &api.Namespace{
Name: "test-namespace",
Description: "Test namespace",
Expand All @@ -123,6 +133,13 @@ meta {
Meta: map[string]string{
"dept": "eng",
},
RequiredExtraClaims: map[string]string{
"foo": "${job.namespace}",
},
OptionalExtraClaims: map[string]string{
"bar": "class:${node.class}",
"baz": "dc:${node.datacenter}",
},
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions nomad/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ func TestACLEndpoint_GetListPolicies_WorkloadIdentity(t *testing.T) {
WorkloadIdentifier: "t",
WorkloadType: structs.WorkloadTypeTask,
},
task.Identity).
task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now().Add(-10 * time.Minute))
jwtToken, _, err := srv.encrypter.SignClaims(claims)
Expand Down Expand Up @@ -2035,7 +2035,7 @@ func TestACLEndpoint_WhoAmI(t *testing.T) {
task := alloc.LookupTask("web")
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc,
wiHandle, // see encrypter_test.go
task.Identity).
task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now().Add(-10 * time.Minute))
jwtToken, _, err := s1.encrypter.SignClaims(claims)
Expand Down
4 changes: 2 additions & 2 deletions nomad/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestAuthenticate_mTLS(t *testing.T) {
task1 := alloc1.LookupTask("web")
claims1 := structs.NewIdentityClaimsBuilder(job, alloc1,
wiHandle,
task1.Identity).
task1.Identity, mock.Namespace()).
WithTask(task1).
Build(time.Now())

Expand All @@ -144,7 +144,7 @@ func TestAuthenticate_mTLS(t *testing.T) {
task2 := alloc2.LookupTask("web")
claims2 := structs.NewIdentityClaimsBuilder(job, alloc2,
wiHandle,
task2.Identity).
task2.Identity, mock.Namespace()).
WithTask(task1).
Build(time.Now())

Expand Down
16 changes: 11 additions & 5 deletions nomad/alloc_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,10 @@ func (a *Alloc) SignIdentities(args *structs.AllocIdentitiesRequest, reply *stru
}

job := out.Job
ns, err := a.srv.State().NamespaceByName(nil, out.Namespace)
if err != nil {
return err
}

switch idReq.WorkloadType {
case structs.WorkloadTypeTask:
Expand All @@ -565,7 +569,7 @@ func (a *Alloc) SignIdentities(args *structs.AllocIdentitiesRequest, reply *stru
continue
}

widFound, err := a.signTasks(task, out, idReq, reply, now)
widFound, err := a.signTasks(task, out, ns, idReq, reply, now)
if err != nil {
return err
}
Expand All @@ -579,7 +583,7 @@ func (a *Alloc) SignIdentities(args *structs.AllocIdentitiesRequest, reply *stru
}

case structs.WorkloadTypeService:
widFound, err := a.signServices(job, out, idReq, reply, now)
widFound, err := a.signServices(job, out, ns, idReq, reply, now)
if err != nil {
return err
}
Expand All @@ -599,6 +603,7 @@ func (a *Alloc) SignIdentities(args *structs.AllocIdentitiesRequest, reply *stru
func (a *Alloc) signTasks(
task *structs.Task,
alloc *structs.Allocation,
ns *structs.Namespace,
idReq *structs.WorkloadIdentityRequest,
reply *structs.AllocIdentitiesResponse,
now time.Time,
Expand All @@ -609,7 +614,7 @@ func (a *Alloc) signTasks(
}

widFound = true
builder := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, &idReq.WIHandle, wid).
builder := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, &idReq.WIHandle, wid, ns).
WithTask(task).
WithConsul()

Expand All @@ -635,6 +640,7 @@ func (a *Alloc) signTasks(
func (a *Alloc) signServices(
job *structs.Job,
alloc *structs.Allocation,
ns *structs.Namespace,
idReq *structs.WorkloadIdentityRequest,
reply *structs.AllocIdentitiesResponse,
now time.Time,
Expand All @@ -646,7 +652,7 @@ func (a *Alloc) signServices(
for _, service := range tg.Services {
if service.IdentityHandle(nil).Equal(wid) {
claims := structs.NewIdentityClaimsBuilder(
alloc.Job, alloc, &idReq.WIHandle, service.Identity).
alloc.Job, alloc, &idReq.WIHandle, service.Identity, ns).
WithConsul().
WithService(service).
Build(now)
Expand All @@ -657,7 +663,7 @@ func (a *Alloc) signServices(
for _, service := range task.Services {
if service.IdentityHandle(nil).Equal(wid) {
claims := structs.NewIdentityClaimsBuilder(
alloc.Job, alloc, &idReq.WIHandle, service.Identity).
alloc.Job, alloc, &idReq.WIHandle, service.Identity, ns).
WithTask(task).
WithConsul().
WithService(service).
Expand Down
17 changes: 17 additions & 0 deletions nomad/alloc_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1828,14 +1828,27 @@ func TestAlloc_SignIdentities_Bad(t *testing.T) {

// Insert an alloc with an alternate identity
alloc := mock.Alloc()
alloc.Job.Meta = map[string]string{"customer": "important"}
alloc.Job.TaskGroups[0].Tasks[0].Identities = []*structs.WorkloadIdentity{
{
Name: "alt",
Audience: []string{"test"},
ExtraClaims: []string{
"customer_claim",
},
},
}
summary := mock.JobSummary(alloc.JobID)
state := s1.fsm.State()
must.NoError(t, state.UpsertNamespaces(100, []*structs.Namespace{{
Name: structs.DefaultNamespace,
RequiredExtraClaims: map[string]string{
"required_claim": "${job.namespace}",
},
OptionalExtraClaims: map[string]string{
"customer_claim": "acct-${job.meta.customer}",
},
}}))
must.NoError(t, state.UpsertJobSummary(100, summary))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 101, nil, alloc.Job))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 101, []*structs.Allocation{alloc}))
Expand All @@ -1861,6 +1874,10 @@ func TestAlloc_SignIdentities_Bad(t *testing.T) {
must.NoError(t, msgpackrpc.CallWithCodec(codec, "Alloc.SignIdentities", &req, &resp))
must.Len(t, 0, resp.Rejections)
must.Len(t, 1, resp.SignedIdentities)
claims, err := s1.encrypter.VerifyClaim(resp.SignedIdentities[0].JWT)
must.NoError(t, err)
must.Eq(t, "default", claims.ExtraClaims["required_claim"])
must.Eq(t, "acct-important", claims.ExtraClaims["customer_claim"])

// Looking for a missing alloc should return a rejection and a signed id
req.Identities = append(req.Identities, &structs.WorkloadIdentityRequest{
Expand Down
8 changes: 5 additions & 3 deletions nomad/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ func TestAuthenticateDefault(t *testing.T) {

claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc,
wih,
identity).
identity,
mock.Namespace()).
Build(time.Now())
auth := testAuthenticator(t, store, true, true)
token, err := auth.encrypter.(*testEncrypter).signClaim(claims)
Expand Down Expand Up @@ -317,7 +318,8 @@ func TestAuthenticateDefault(t *testing.T) {
alloc.ClientStatus = structs.AllocClientStatusRunning
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc,
wih,
identity).
identity,
mock.Namespace()).
Build(time.Now())

auth := testAuthenticator(t, store, true, true)
Expand Down Expand Up @@ -1295,7 +1297,7 @@ func TestIdentityToACLClaim(t *testing.T) {
defaultWI := &structs.WorkloadIdentity{Name: "default"}
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc,
task.IdentityHandle(defaultWI),
task.Identity).
task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now())

Expand Down
6 changes: 3 additions & 3 deletions nomad/encrypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ func TestEncrypter_SignVerify(t *testing.T) {
alloc := mock.Alloc()
task := alloc.LookupTask("web")

claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity).
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now())
e := srv.encrypter
Expand Down Expand Up @@ -721,7 +721,7 @@ func TestEncrypter_SignVerify_Issuer(t *testing.T) {

alloc := mock.Alloc()
task := alloc.LookupTask("web")
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity).
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now())

Expand Down Expand Up @@ -750,7 +750,7 @@ func TestEncrypter_SignVerify_AlgNone(t *testing.T) {

alloc := mock.Alloc()
task := alloc.LookupTask("web")
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity).
claims := structs.NewIdentityClaimsBuilder(alloc.Job, alloc, wiHandle, task.Identity, mock.Namespace()).
WithTask(task).
Build(time.Now())

Expand Down
11 changes: 8 additions & 3 deletions nomad/plan_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,12 @@ func (p *planner) applyPlan(plan *structs.Plan, result *structs.PlanResult, snap
// to approximate the scheduling time.
updateAllocTimestamps(req.AllocsUpdated, unixNow)

if err := signAllocIdentities(p.srv.encrypter, job, req.AllocsUpdated, now); err != nil {
ns, err := snap.NamespaceByName(nil, job.Namespace)
if err != nil {
return nil, err
}

if err := signAllocIdentities(p.srv.encrypter, job, req.AllocsUpdated, ns, now); err != nil {
return nil, err
}

Expand Down Expand Up @@ -374,7 +379,7 @@ func updateAllocTimestamps(allocations []*structs.Allocation, timestamp int64) {
}
}

func signAllocIdentities(signer claimSigner, job *structs.Job, allocations []*structs.Allocation, now time.Time) error {
func signAllocIdentities(signer claimSigner, job *structs.Job, allocations []*structs.Allocation, ns *structs.Namespace, now time.Time) error {
for _, alloc := range allocations {
if alloc.SignedIdentities == nil {
alloc.SignedIdentities = map[string]string{}
Expand All @@ -388,7 +393,7 @@ func signAllocIdentities(signer claimSigner, job *structs.Job, allocations []*st
defaultWI := &structs.WorkloadIdentity{Name: "default"}

claims := structs.NewIdentityClaimsBuilder(
job, alloc, task.IdentityHandle(defaultWI), task.Identity).
job, alloc, task.IdentityHandle(defaultWI), task.Identity, ns).
WithTask(task).
Build(now)

Expand Down
Loading
Loading