Skip to content

Commit c531e41

Browse files
authored
Merge branch 'main' into delete_non_lru_for_reals
2 parents 8b598d7 + 9f002a4 commit c531e41

File tree

5 files changed

+248
-35
lines changed

5 files changed

+248
-35
lines changed

doc/spire_agent.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,31 @@ plugins {
367367

368368
## Delegated Identity API
369369

370-
The Delegated Identity API allows an authorized (i.e. delegated) workload to obtain SVIDs and bundles on behalf of workloads that cannot be attested by SPIRE Agent directly. The authorized workload does so by providing SPIRE Agent the selectors that would normally be obtained during workload attestation. The Delegated Identity API is served over the admin API endpoint.
370+
The Delegated Identity API allows an authorized (i.e. delegated) workload to obtain SVIDs and bundles on behalf of workloads that cannot be attested by SPIRE Agent directly.
371+
372+
The Delegated Identity API is served over the SPIRE Agent's admin API endpoint.
373+
374+
Note that this explicitly and by-design grants the authorized delegate workload the ability to impersonate any of the other workloads it can obtain SVIDs for. Any workload authorized to use the
375+
Delegated Identity API becomes a "trusted delegate" of the SPIRE Agent, and may impersonate and act on behalf of all workload SVIDs it obtains from the SPIRE Agent.
376+
377+
The trusted delegate workload itself is attested by the SPIRE Agent first, and the delegate's SPIFFE ID is checked against an allowlist of authorized delegates.
378+
379+
Once these requirements are met, the trusted delegate workload can obtain SVIDS for any workloads in the scope of the SPIRE Agent instance it is interacting with.
380+
381+
There are two ways the trusted delegate workload can request SVIDs for other workloads from the SPIRE Agent:
382+
383+
1. By attesting the other workload itself, building a set of selectors, and then providing SPIRE Agent those selectors over the Delegated Identity API.
384+
In this approach, the trusted delegate workload is entirely responsible for attesting the other workload and building the attested selectors.
385+
When those selectors are presented to the SPIRE Agent, the SPIRE Agent will simply return SVIDs for any workload registration entries that match the provided selectors.
386+
No other checks or attestations will be performed by the SPIRE Agent.
387+
388+
1. By obtaining a PID for the other workload, and providing that PID to the SPIRE Agent over the Delegated Identity API.
389+
In this approach, the SPIRE Agent will do attestation for the provided PID, build the attested selectors, and return SVIDs for any workload registration entries that match the selectors the SPIRE Agent attested from that PID.
390+
This differs from the previous approach in that the SPIRE Agent itself (not the trusted delegate) handles the attestation of the other workload.
391+
On most platforms PIDs are not stable identifiers, so the trusted delegate workload **must** ensure that the PID it provides to the SPIRE Agent
392+
via the Delegated Identity API for attestation is not recycled between the time a trusted delegate makes an Delegate Identity API request, and obtains a Delegate Identity API response.
393+
How this is accomplished is platform-dependent and the responsibility of the trusted delegate (e.g. by using pidfds on Linux).
394+
Attestation results obtained via the Delegated Identity API for a PID are valid until the process referred to by the PID terminates, or is re-attested - whichever comes first.
371395

372396
To enable the Delegated Identity API, configure the admin API endpoint address and the list of SPIFFE IDs for authorized delegates. For example:
373397

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ require (
7171
github.com/sigstore/sigstore v1.8.8
7272
github.com/sirupsen/logrus v1.9.3
7373
github.com/spiffe/go-spiffe/v2 v2.3.0
74-
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b
74+
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35
7575
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d
7676
github.com/stretchr/testify v1.9.0
7777
github.com/uber-go/tally/v4 v4.1.16

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,8 +1426,8 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+
14261426
github.com/spiffe/go-spiffe/v2 v2.1.6/go.mod h1:eVDqm9xFvyqao6C+eQensb9ZPkyNEeaUbqbBpOhBnNk=
14271427
github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8=
14281428
github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY=
1429-
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b h1:k7ei1fQyt6+FbqDEAd90xaXLg52YuXueM+BRcoHZvEU=
1430-
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI=
1429+
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35 h1:Ah7jJvfjw2fYXtSJF69lWokspl5Vhge0yiSi/mFhzhM=
1430+
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI=
14311431
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d h1:LCRQGU6vOqKLfRrG+GJQrwMwDILcAddAEIf4/1PaSVc=
14321432
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d/go.mod h1:GA6o2PVLwyJdevT6KKt5ZXCY/ziAPna13y/seGk49Ik=
14331433
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

pkg/agent/api/delegatedidentity/v1/service.go

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,22 @@ func New(config Config) *Service {
5454
}
5555

5656
return &Service{
57-
manager: config.Manager,
58-
attestor: endpoints.PeerTrackerAttestor{Attestor: config.Attestor},
59-
metrics: config.Metrics,
60-
authorizedDelegates: AuthorizedDelegates,
57+
manager: config.Manager,
58+
peerAttestor: endpoints.PeerTrackerAttestor{Attestor: config.Attestor},
59+
delegateWorkloadAttestor: config.Attestor,
60+
metrics: config.Metrics,
61+
authorizedDelegates: AuthorizedDelegates,
6162
}
6263
}
6364

6465
// Service implements the delegated identity server
6566
type Service struct {
6667
delegatedidentityv1.UnsafeDelegatedIdentityServer
6768

68-
manager manager.Manager
69-
attestor attestor
70-
metrics telemetry.Metrics
69+
manager manager.Manager
70+
peerAttestor attestor
71+
delegateWorkloadAttestor workloadattestor.Attestor
72+
metrics telemetry.Metrics
7173

7274
// SPIFFE IDs of delegates that are authorized to use this API
7375
authorizedDelegates map[string]bool
@@ -79,7 +81,7 @@ func (s *Service) isCallerAuthorized(ctx context.Context, log logrus.FieldLogger
7981
callerSelectors := cachedSelectors
8082

8183
if callerSelectors == nil {
82-
callerSelectors, err = s.attestor.Attest(ctx)
84+
callerSelectors, err = s.peerAttestor.Attest(ctx)
8385
if err != nil {
8486
log.WithError(err).Error("Workload attestation failed")
8587
return nil, status.Error(codes.Internal, "workload attestation failed")
@@ -111,6 +113,53 @@ func (s *Service) isCallerAuthorized(ctx context.Context, log logrus.FieldLogger
111113
return nil, status.Error(codes.PermissionDenied, "caller not configured as an authorized delegate")
112114
}
113115

116+
func (s *Service) constructValidSelectorsFromReq(ctx context.Context, log logrus.FieldLogger, reqPid int32, reqSelectors []*types.Selector) ([]*common.Selector, error) {
117+
// If you set
118+
// - both pid and selector args
119+
// - neither of them
120+
// it's an error
121+
// NOTE: the default value of int32 is naturally 0 in protobuf, which is also a valid PID.
122+
// However, we will still treat that as an error, as we do not expect to ever be asked to attest
123+
// pid 0.
124+
125+
if (len(reqSelectors) != 0 && reqPid != 0) || (len(reqSelectors) == 0 && reqPid == 0) {
126+
log.Error("Invalid argument; must provide either selectors or non-zero PID, but not both")
127+
return nil, status.Error(codes.InvalidArgument, "must provide either selectors or non-zero PID, but not both")
128+
}
129+
130+
var selectors []*common.Selector
131+
var err error
132+
133+
if len(reqSelectors) != 0 {
134+
// Delegate authorized, if the delegate gives us selectors, we treat them as attested.
135+
selectors, err = api.SelectorsFromProto(reqSelectors)
136+
if err != nil {
137+
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
138+
return nil, status.Error(codes.InvalidArgument, "could not parse provided selectors")
139+
}
140+
} else {
141+
// Delegate authorized, use PID the delegate gave us to try and attest on-behalf-of
142+
selectors, err = s.delegateWorkloadAttestor.Attest(ctx, int(reqPid))
143+
if err != nil {
144+
return nil, err
145+
}
146+
}
147+
148+
return selectors, nil
149+
}
150+
151+
// Attempt to attest and authorize the delegate, and then
152+
//
153+
// - Take a pre-atttested set of selectors from the delegate
154+
// - the PID the delegate gave us and attempt to attest that into a set of selectors
155+
//
156+
// and provide a SVID subscription for those selectors.
157+
//
158+
// NOTE:
159+
// - If supplying a PID, the trusted delegate is responsible for ensuring the PID is valid and not recycled,
160+
// from initiation of this call until the termination of the response stream, and if it is,
161+
// must discard any stream contents provided by this call as invalid.
162+
// - If supplying selectors, the trusted delegate is responsible for ensuring they are correct.
114163
func (s *Service) SubscribeToX509SVIDs(req *delegatedidentityv1.SubscribeToX509SVIDsRequest, stream delegatedidentityv1.DelegatedIdentity_SubscribeToX509SVIDsServer) error {
115164
latency := adminapi.StartFirstX509SVIDUpdateLatency(s.metrics)
116165
ctx := stream.Context()
@@ -122,10 +171,9 @@ func (s *Service) SubscribeToX509SVIDs(req *delegatedidentityv1.SubscribeToX509S
122171
return err
123172
}
124173

125-
selectors, err := api.SelectorsFromProto(req.Selectors)
174+
selectors, err := s.constructValidSelectorsFromReq(ctx, log, req.Pid, req.Selectors)
126175
if err != nil {
127-
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
128-
return status.Error(codes.InvalidArgument, "could not parse provided selectors")
176+
return err
129177
}
130178

131179
log.WithFields(logrus.Fields{
@@ -290,6 +338,18 @@ func (s *Service) SubscribeToX509Bundles(_ *delegatedidentityv1.SubscribeToX509B
290338
}
291339
}
292340

341+
// Attempt to attest and authorize the delegate, and then
342+
//
343+
// - Take a pre-atttested set of selectors from the delegate
344+
// - the PID the delegate gave us and attempt to attest that into a set of selectors
345+
//
346+
// and provide a JWT SVID for those selectors.
347+
//
348+
// NOTE:
349+
// - If supplying a PID, the trusted delegate is responsible for ensuring the PID is valid and not recycled,
350+
// from initiation of this call until the response is returned, and if it is,
351+
// must discard any response provided by this call as invalid.
352+
// - If supplying selectors, the trusted delegate is responsible for ensuring they are correct.
293353
func (s *Service) FetchJWTSVIDs(ctx context.Context, req *delegatedidentityv1.FetchJWTSVIDsRequest) (resp *delegatedidentityv1.FetchJWTSVIDsResponse, err error) {
294354
log := rpccontext.Logger(ctx)
295355
if len(req.Audience) == 0 {
@@ -301,10 +361,9 @@ func (s *Service) FetchJWTSVIDs(ctx context.Context, req *delegatedidentityv1.Fe
301361
return nil, err
302362
}
303363

304-
selectors, err := api.SelectorsFromProto(req.Selectors)
364+
selectors, err := s.constructValidSelectorsFromReq(ctx, log, req.Pid, req.Selectors)
305365
if err != nil {
306-
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
307-
return nil, status.Error(codes.InvalidArgument, "could not parse provided selectors")
366+
return nil, err
308367
}
309368

310369
resp = new(delegatedidentityv1.FetchJWTSVIDsResponse)

0 commit comments

Comments
 (0)