Skip to content

Commit 1d8392e

Browse files
committed
Add CLI and entry conversion support for JWT-SVID audience policies
This commit adds support for the new JWT-SVID audience policy configuration in the SPIRE server CLI and entry conversion logic: CLI changes: - Add -jwtSVIDDefaultAudiencePolicy flag for default audience policy - Add -jwtSVIDAudiencePolicy flag for per-audience policy configuration - Update entry create, update, and show commands to handle new fields - Add AudiencePolicyFlag custom type for parsing audience:policy pairs Entry conversion: - Add JwtSvidDefaultAudiencePolicy and JwtSvidAudiencePolicies to EntryToProto and ProtoToEntry conversion functions - Add audiencePolicyToInternal helper for enum conversion Policy options: default, auditable, unique - default: No JTI claim, caching enabled (current behavior) - auditable: JTI claim included, caching enabled - unique: JTI claim included, caching disabled (unique tokens) Part of spiffe#6043 NOTE: Only merge after these dependent PRs are merged: - spire-api-sdk: spiffe/spire-api-sdk#84 - spire-plugin-sdk: https://github.com/spiffe/spire-plugin-sdk/pull/113
1 parent f45e5a4 commit 1d8392e

File tree

13 files changed

+289
-84
lines changed

13 files changed

+289
-84
lines changed

cmd/spire-server/cli/entry/create.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ type createCommand struct {
7474
// storeSVID determines if the issued SVID must be stored through an SVIDStore plugin
7575
storeSVID bool
7676

77+
// JWT-SVID default audience policy
78+
jwtSVIDDefaultAudiencePolicy string
79+
80+
// JWT-SVID per-audience policies
81+
jwtSVIDAudiencePolicies AudiencePolicyFlag
82+
7783
printer cliprinter.Printer
7884

7985
env *commoncli.Env
@@ -103,6 +109,8 @@ func (c *createCommand) AppendFlags(f *flag.FlagSet) {
103109
f.Int64Var(&c.entryExpiry, "entryExpiry", 0, "An expiry, from epoch in seconds, for the resulting registration entry to be pruned")
104110
f.Var(&c.dnsNames, "dns", "A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once")
105111
f.StringVar(&c.hint, "hint", "", "The entry hint, used to disambiguate entries with the same SPIFFE ID")
112+
f.StringVar(&c.jwtSVIDDefaultAudiencePolicy, "jwtSVIDDefaultAudiencePolicy", "", "Default JWT-SVID audience policy for audiences not explicitly configured. One of: default, auditable, unique")
113+
f.Var(&c.jwtSVIDAudiencePolicies, "jwtSVIDAudiencePolicy", "Per-audience JWT-SVID policy in the format 'audience:policy' where policy is one of: default, auditable, unique. Can be used more than once")
106114
cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, prettyPrintCreate)
107115
}
108116

@@ -187,17 +195,24 @@ func (c *createCommand) parseConfig() ([]*types.Entry, error) {
187195
return nil, fmt.Errorf("invalid value for JWT SVID TTL: %w", err)
188196
}
189197

198+
jwtSvidDefaultAudiencePolicy, err := parseJWTSVIDAudiencePolicy(c.jwtSVIDDefaultAudiencePolicy)
199+
if err != nil {
200+
return nil, err
201+
}
202+
190203
e := &types.Entry{
191-
Id: c.entryID,
192-
ParentId: parentID,
193-
SpiffeId: spiffeID,
194-
Downstream: c.downstream,
195-
ExpiresAt: c.entryExpiry,
196-
DnsNames: c.dnsNames,
197-
StoreSvid: c.storeSVID,
198-
X509SvidTtl: x509SvidTTL,
199-
JwtSvidTtl: jwtSvidTTL,
200-
Hint: c.hint,
204+
Id: c.entryID,
205+
ParentId: parentID,
206+
SpiffeId: spiffeID,
207+
Downstream: c.downstream,
208+
ExpiresAt: c.entryExpiry,
209+
DnsNames: c.dnsNames,
210+
StoreSvid: c.storeSVID,
211+
X509SvidTtl: x509SvidTTL,
212+
JwtSvidTtl: jwtSvidTTL,
213+
Hint: c.hint,
214+
JwtSvidDefaultAudiencePolicy: jwtSvidDefaultAudiencePolicy,
215+
JwtSvidAudiencePolicies: c.jwtSVIDAudiencePolicies,
201216
}
202217

203218
selectors := []*types.Selector{}

cmd/spire-server/cli/entry/create_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,9 @@ StoreSvid : true
319319
],
320320
"revision_number": "0",
321321
"store_svid": true,
322-
"jwt_svid_ttl": 30
322+
"jwt_svid_ttl": 30,
323+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
324+
"jwt_svid_audience_policies": {}
323325
}
324326
}
325327
]
@@ -426,7 +428,9 @@ StoreSvid : true
426428
],
427429
"revision_number": "0",
428430
"store_svid": true,
429-
"jwt_svid_ttl": 0
431+
"jwt_svid_ttl": 0,
432+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
433+
"jwt_svid_audience_policies": {}
430434
}
431435
}
432436
]
@@ -531,7 +535,9 @@ StoreSvid : true
531535
"dns_names": [],
532536
"revision_number": "0",
533537
"store_svid": false,
534-
"jwt_svid_ttl": 30
538+
"jwt_svid_ttl": 30,
539+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
540+
"jwt_svid_audience_policies": {}
535541
}
536542
},
537543
{
@@ -565,7 +571,9 @@ StoreSvid : true
565571
"dns_names": [],
566572
"revision_number": "0",
567573
"store_svid": false,
568-
"jwt_svid_ttl": 30
574+
"jwt_svid_ttl": 30,
575+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
576+
"jwt_svid_audience_policies": {}
569577
}
570578
},
571579
{
@@ -603,7 +611,9 @@ StoreSvid : true
603611
"dns_names": [],
604612
"revision_number": "0",
605613
"store_svid": true,
606-
"jwt_svid_ttl": 30
614+
"jwt_svid_ttl": 30,
615+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
616+
"jwt_svid_audience_policies": {}
607617
}
608618
}
609619
]
@@ -664,7 +674,9 @@ Error: failed to create one or more entries
664674
"dns_names": [],
665675
"revision_number": "0",
666676
"store_svid": false,
667-
"jwt_svid_ttl": 0
677+
"jwt_svid_ttl": 0,
678+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
679+
"jwt_svid_audience_policies": {}
668680
}
669681
}
670682
]

cmd/spire-server/cli/entry/show_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,9 @@ func getJSONPrintedEntry(idx int) string {
534534
"dns_names": [],
535535
"revision_number": "0",
536536
"store_svid": false,
537-
"jwt_svid_ttl": 0
537+
"jwt_svid_ttl": 0,
538+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
539+
"jwt_svid_audience_policies": {}
538540
}`
539541
case 1:
540542
return `{
@@ -567,7 +569,9 @@ func getJSONPrintedEntry(idx int) string {
567569
"dns_names": [],
568570
"revision_number": "0",
569571
"store_svid": false,
570-
"jwt_svid_ttl": 0
572+
"jwt_svid_ttl": 0,
573+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
574+
"jwt_svid_audience_policies": {}
571575
}`
572576
case 2:
573577
return `{
@@ -602,7 +606,9 @@ func getJSONPrintedEntry(idx int) string {
602606
"dns_names": [],
603607
"revision_number": "0",
604608
"store_svid": false,
605-
"jwt_svid_ttl": 0
609+
"jwt_svid_ttl": 0,
610+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
611+
"jwt_svid_audience_policies": {}
606612
}`
607613
case 3:
608614
return `{
@@ -631,7 +637,9 @@ func getJSONPrintedEntry(idx int) string {
631637
"dns_names": [],
632638
"revision_number": "0",
633639
"store_svid": false,
634-
"jwt_svid_ttl": 0
640+
"jwt_svid_ttl": 0,
641+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
642+
"jwt_svid_audience_policies": {}
635643
}`
636644
default:
637645
return "index should be lower than 4"

cmd/spire-server/cli/entry/update.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ type updateCommand struct {
7070
// Entry hint, used to disambiguate entries with the same SPIFFE ID
7171
hint string
7272

73+
// JWT-SVID default audience policy
74+
jwtSVIDDefaultAudiencePolicy string
75+
76+
// JWT-SVID per-audience policies
77+
jwtSVIDAudiencePolicies AudiencePolicyFlag
78+
7379
printer cliprinter.Printer
7480

7581
env *commoncli.Env
@@ -98,6 +104,8 @@ func (c *updateCommand) AppendFlags(f *flag.FlagSet) {
98104
f.Int64Var(&c.entryExpiry, "entryExpiry", 0, "An expiry, from epoch in seconds, for the resulting registration entry to be pruned")
99105
f.Var(&c.dnsNames, "dns", "A DNS name that will be included in SVIDs issued based on this entry, where appropriate. Can be used more than once")
100106
f.StringVar(&c.hint, "hint", "", "The entry hint, used to disambiguate entries with the same SPIFFE ID")
107+
f.StringVar(&c.jwtSVIDDefaultAudiencePolicy, "jwtSVIDDefaultAudiencePolicy", "", "Default JWT-SVID audience policy for audiences not explicitly configured. One of: default, auditable, unique")
108+
f.Var(&c.jwtSVIDAudiencePolicies, "jwtSVIDAudiencePolicy", "Per-audience JWT-SVID policy in the format 'audience:policy' where policy is one of: default, auditable, unique. Can be used more than once")
101109
cliprinter.AppendFlagWithCustomPretty(&c.printer, f, c.env, prettyPrintUpdate)
102110
}
103111

@@ -181,16 +189,23 @@ func (c *updateCommand) parseConfig() ([]*types.Entry, error) {
181189
return nil, fmt.Errorf("invalid value for JWT SVID TTL: %w", err)
182190
}
183191

192+
jwtSvidDefaultAudiencePolicy, err := parseJWTSVIDAudiencePolicy(c.jwtSVIDDefaultAudiencePolicy)
193+
if err != nil {
194+
return nil, err
195+
}
196+
184197
e := &types.Entry{
185-
Id: c.entryID,
186-
ParentId: parentID,
187-
SpiffeId: spiffeID,
188-
Downstream: c.downstream,
189-
ExpiresAt: c.entryExpiry,
190-
DnsNames: c.dnsNames,
191-
X509SvidTtl: x509SvidTTL,
192-
JwtSvidTtl: jwtSvidTTL,
193-
Hint: c.hint,
198+
Id: c.entryID,
199+
ParentId: parentID,
200+
SpiffeId: spiffeID,
201+
Downstream: c.downstream,
202+
ExpiresAt: c.entryExpiry,
203+
DnsNames: c.dnsNames,
204+
X509SvidTtl: x509SvidTTL,
205+
JwtSvidTtl: jwtSvidTTL,
206+
Hint: c.hint,
207+
JwtSvidDefaultAudiencePolicy: jwtSvidDefaultAudiencePolicy,
208+
JwtSvidAudiencePolicies: c.jwtSVIDAudiencePolicies,
194209
}
195210

196211
selectors := []*types.Selector{}

cmd/spire-server/cli/entry/update_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ func TestUpdate(t *testing.T) {
6161
],
6262
"revision_number": "0",
6363
"store_svid": true,
64-
"jwt_svid_ttl": 30
64+
"jwt_svid_ttl": 30,
65+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
66+
"jwt_svid_audience_policies": {}
6567
}`
6668
entry0AdminJSON := `{
6769
"id": "entry-id",
@@ -99,7 +101,9 @@ func TestUpdate(t *testing.T) {
99101
],
100102
"revision_number": "0",
101103
"store_svid": false,
102-
"jwt_svid_ttl": 30
104+
"jwt_svid_ttl": 30,
105+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
106+
"jwt_svid_audience_policies": {}
103107
}`
104108
entry1JSON := `{
105109
"id": "entry-id-1",
@@ -127,7 +131,9 @@ func TestUpdate(t *testing.T) {
127131
"dns_names": [],
128132
"revision_number": "0",
129133
"store_svid": false,
130-
"jwt_svid_ttl": 300
134+
"jwt_svid_ttl": 300,
135+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
136+
"jwt_svid_audience_policies": {}
131137
}
132138
}`
133139
entry2JSON := `{
@@ -156,7 +162,9 @@ func TestUpdate(t *testing.T) {
156162
"dns_names": [],
157163
"revision_number": "0",
158164
"store_svid": false,
159-
"jwt_svid_ttl": 300
165+
"jwt_svid_ttl": 300,
166+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
167+
"jwt_svid_audience_policies": {}
160168
}
161169
}`
162170
entry3JSON := `{
@@ -189,7 +197,9 @@ func TestUpdate(t *testing.T) {
189197
"dns_names": [],
190198
"revision_number": "0",
191199
"store_svid": true,
192-
"jwt_svid_ttl": 300
200+
"jwt_svid_ttl": 300,
201+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
202+
"jwt_svid_audience_policies": {}
193203
}`
194204
nonExistentEntryJSON := `{
195205
"id": "non-existent-id",
@@ -217,7 +227,9 @@ func TestUpdate(t *testing.T) {
217227
"dns_names": [],
218228
"revision_number": "0",
219229
"store_svid": false,
220-
"x509_svid_ttl": 0
230+
"x509_svid_ttl": 0,
231+
"jwt_svid_default_audience_policy": "JWT_SVID_AUDIENCE_POLICY_DEFAULT",
232+
"jwt_svid_audience_policies": {}
221233
}`
222234

223235
entry1 := &types.Entry{

cmd/spire-server/cli/entry/util.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"sort"
9+
"strings"
810
"time"
911

1012
"github.com/spiffe/go-spiffe/v2/spiffeid"
@@ -63,6 +65,23 @@ func printEntry(e *types.Entry, printf func(string, ...any) error) {
6365
_ = printf("Hint : %s\n", e.Hint)
6466
}
6567

68+
// Only show JWT-SVID audience policies if configured
69+
if e.JwtSvidDefaultAudiencePolicy != types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_DEFAULT {
70+
_ = printf("JWT Default Policy: %s\n", jwtSVIDAudiencePolicyName(e.JwtSvidDefaultAudiencePolicy))
71+
}
72+
if len(e.JwtSvidAudiencePolicies) > 0 {
73+
// Sort audiences for consistent output
74+
audiences := make([]string, 0, len(e.JwtSvidAudiencePolicies))
75+
for aud := range e.JwtSvidAudiencePolicies {
76+
audiences = append(audiences, aud)
77+
}
78+
sort.Strings(audiences)
79+
for _, aud := range audiences {
80+
policy := e.JwtSvidAudiencePolicies[aud]
81+
_ = printf("JWT Aud Policy : %s:%s\n", aud, jwtSVIDAudiencePolicyName(policy))
82+
}
83+
}
84+
6685
_ = printf("\n")
6786
}
6887

@@ -137,3 +156,63 @@ func (s *StringsFlag) Set(val string) error {
137156
*s = append(*s, val)
138157
return nil
139158
}
159+
160+
// AudiencePolicyFlag defines a custom type for audience:policy pairs.
161+
// Format: "audience:policy" where policy is one of: default, auditable, unique
162+
type AudiencePolicyFlag map[string]types.JWTSVIDAudiencePolicy
163+
164+
// String returns the string representation of the flag.
165+
func (a *AudiencePolicyFlag) String() string {
166+
return fmt.Sprint(*a)
167+
}
168+
169+
// Set parses and appends an audience:policy pair.
170+
func (a *AudiencePolicyFlag) Set(val string) error {
171+
parts := strings.SplitN(val, ":", 2)
172+
if len(parts) != 2 {
173+
return fmt.Errorf("invalid audience policy format %q, expected audience:policy", val)
174+
}
175+
176+
audience := parts[0]
177+
if audience == "" {
178+
return fmt.Errorf("audience cannot be empty in %q", val)
179+
}
180+
181+
policy, err := parseJWTSVIDAudiencePolicy(parts[1])
182+
if err != nil {
183+
return err
184+
}
185+
186+
if *a == nil {
187+
*a = make(map[string]types.JWTSVIDAudiencePolicy)
188+
}
189+
(*a)[audience] = policy
190+
return nil
191+
}
192+
193+
// parseJWTSVIDAudiencePolicy parses a policy string into the enum value.
194+
func parseJWTSVIDAudiencePolicy(s string) (types.JWTSVIDAudiencePolicy, error) {
195+
switch strings.ToLower(s) {
196+
case "default", "":
197+
return types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_DEFAULT, nil
198+
case "auditable":
199+
return types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_AUDITABLE, nil
200+
case "unique":
201+
return types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_UNIQUE, nil
202+
default:
203+
return types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_DEFAULT,
204+
fmt.Errorf("invalid JWT-SVID audience policy %q, must be one of: default, auditable, unique", s)
205+
}
206+
}
207+
208+
// jwtSVIDAudiencePolicyName returns the human-readable name of the policy.
209+
func jwtSVIDAudiencePolicyName(p types.JWTSVIDAudiencePolicy) string {
210+
switch p {
211+
case types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_AUDITABLE:
212+
return "auditable"
213+
case types.JWTSVIDAudiencePolicy_JWT_SVID_AUDIENCE_POLICY_UNIQUE:
214+
return "unique"
215+
default:
216+
return "default"
217+
}
218+
}

0 commit comments

Comments
 (0)