Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow DelegatedIdentity API clients to subscribe by PID #5272

Merged
merged 21 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 17 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
26 changes: 25 additions & 1 deletion doc/spire_agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,31 @@ plugins {

## Delegated Identity API

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.
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 Delegated Identity API is served over the SPIRE Agent's admin API endpoint.

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
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.

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.

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.

There are two ways the trusted delegate workload can request SVIDs for other workloads from the SPIRE Agent:

1. By attesting the other workload itself, building a set of selectors, and then providing SPIRE Agent those selectors over the Delegated Identity API.
In this approach, the trusted delegate workload is entirely responsible for attesting the other workload and building the attested selectors.
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.
No other checks or attestations will be performed by the SPIRE Agent.

1. By obtaining a PID for the other workload, and providing that PID to the SPIRE Agent over the Delegated Identity API.
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.
This differs from the previous approach in that the SPIRE Agent itself (not the trusted delegate) handles the attestation of the other workload.
On most platforms PIDs are not stable identifiers, so the trusted delegate workload **must** ensure that the PID it provides to the SPIRE Agent
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.
How this is accomplished is platform-dependent and the responsibility of the trusted delegate (e.g. by using pidfds on Linux).
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.

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

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ require (
github.com/sigstore/sigstore v1.8.7
github.com/sirupsen/logrus v1.9.3
github.com/spiffe/go-spiffe/v2 v2.3.0
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d
github.com/stretchr/testify v1.9.0
github.com/uber-go/tally/v4 v4.1.16
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1420,8 +1420,8 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
github.com/spiffe/go-spiffe/v2 v2.1.6/go.mod h1:eVDqm9xFvyqao6C+eQensb9ZPkyNEeaUbqbBpOhBnNk=
github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8=
github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY=
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b h1:k7ei1fQyt6+FbqDEAd90xaXLg52YuXueM+BRcoHZvEU=
github.com/spiffe/spire-api-sdk v1.2.5-0.20240627195926-b5ac064f580b/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI=
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35 h1:Ah7jJvfjw2fYXtSJF69lWokspl5Vhge0yiSi/mFhzhM=
github.com/spiffe/spire-api-sdk v1.2.5-0.20240722174251-0116a7186c35/go.mod h1:4uuhFlN6KBWjACRP3xXwrOTNnvaLp1zJs8Lribtr4fI=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d h1:LCRQGU6vOqKLfRrG+GJQrwMwDILcAddAEIf4/1PaSVc=
github.com/spiffe/spire-plugin-sdk v1.4.4-0.20230721151831-bf67dde4721d/go.mod h1:GA6o2PVLwyJdevT6KKt5ZXCY/ziAPna13y/seGk49Ik=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
89 changes: 75 additions & 14 deletions pkg/agent/api/delegatedidentity/v1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@ func New(config Config) *Service {
}

return &Service{
manager: config.Manager,
attestor: endpoints.PeerTrackerAttestor{Attestor: config.Attestor},
metrics: config.Metrics,
authorizedDelegates: AuthorizedDelegates,
manager: config.Manager,
peerAttestor: endpoints.PeerTrackerAttestor{Attestor: config.Attestor},
delegateWorkloadAttestor: config.Attestor,
metrics: config.Metrics,
authorizedDelegates: AuthorizedDelegates,
}
}

// Service implements the delegated identity server
type Service struct {
delegatedidentityv1.UnsafeDelegatedIdentityServer

manager manager.Manager
attestor attestor
metrics telemetry.Metrics
manager manager.Manager
peerAttestor attestor
delegateWorkloadAttestor workloadattestor.Attestor
metrics telemetry.Metrics

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

if callerSelectors == nil {
callerSelectors, err = s.attestor.Attest(ctx)
callerSelectors, err = s.peerAttestor.Attest(ctx)
if err != nil {
log.WithError(err).Error("Workload attestation failed")
return nil, status.Error(codes.Internal, "workload attestation failed")
Expand Down Expand Up @@ -111,6 +113,55 @@ func (s *Service) isCallerAuthorized(ctx context.Context, log logrus.FieldLogger
return nil, status.Error(codes.PermissionDenied, "caller not configured as an authorized delegate")
}

func (s *Service) constructValidSelectorsFromReq(ctx context.Context, log logrus.FieldLogger, reqPid int32, reqSelectors []*types.Selector) ([]*common.Selector, error) {
// If you set
// - both pid and selector args
// - neither of them
// it's an error
// NOTE: the default value of int32 is naturally 0 in protobuf, which is also a valid PID.
// However, we will still treat that as an error, as we do not expect to ever be asked to attest
// pid 0.

fmt.Printf("PIDS: %d", reqPid)
bleggett marked this conversation as resolved.
Show resolved Hide resolved
if (len(reqSelectors) != 0 && reqPid != 0) || (len(reqSelectors) == 0 && reqPid == 0) {
fmt.Printf("booop")
bleggett marked this conversation as resolved.
Show resolved Hide resolved
log.Error("Invalid argument; must provide either selectors or non-zero PID, but not both")
return nil, status.Error(codes.InvalidArgument, "must provide either selectors or non-zero PID, but not both")
}

var selectors []*common.Selector
var err error

if len(reqSelectors) != 0 {
// Delegate authorized, if the delegate gives us selectors, we treat them as attested.
selectors, err = api.SelectorsFromProto(reqSelectors)
if err != nil {
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
return nil, status.Error(codes.InvalidArgument, "could not parse provided selectors")
}
} else {
// Delegate authorized, use PID the delegate gave us to try and attest on-behalf-of
selectors, err = s.delegateWorkloadAttestor.Attest(ctx, int(reqPid))
if err != nil {
return nil, err
}
}

return selectors, nil
}

// Attempt to attest and authorize the delegate, and then
//
// - Take a pre-atttested set of selectors from the delegate
// - the PID the delegate gave us and attempt to attest that into a set of selectors
//
// and provide a SVID subscription for those selectors.
//
// NOTE:
// - If supplying a PID, the trusted delegate is responsible for ensuring the PID is valid and not recycled,
// from initiation of this call until the termination of the response stream, and if it is,
// must discard any stream contents provided by this call as invalid.
// - If supplying selectors, the trusted delegate is responsible for ensuring they are correct.
func (s *Service) SubscribeToX509SVIDs(req *delegatedidentityv1.SubscribeToX509SVIDsRequest, stream delegatedidentityv1.DelegatedIdentity_SubscribeToX509SVIDsServer) error {
latency := adminapi.StartFirstX509SVIDUpdateLatency(s.metrics)
ctx := stream.Context()
Expand All @@ -122,10 +173,9 @@ func (s *Service) SubscribeToX509SVIDs(req *delegatedidentityv1.SubscribeToX509S
return err
}

selectors, err := api.SelectorsFromProto(req.Selectors)
selectors, err := s.constructValidSelectorsFromReq(ctx, log, req.Pid, req.Selectors)
if err != nil {
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
return status.Error(codes.InvalidArgument, "could not parse provided selectors")
return err
}

log.WithFields(logrus.Fields{
Expand Down Expand Up @@ -290,6 +340,18 @@ func (s *Service) SubscribeToX509Bundles(_ *delegatedidentityv1.SubscribeToX509B
}
}

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

selectors, err := api.SelectorsFromProto(req.Selectors)
selectors, err := s.constructValidSelectorsFromReq(ctx, log, req.Pid, req.Selectors)
if err != nil {
log.WithError(err).Error("Invalid argument; could not parse provided selectors")
return nil, status.Error(codes.InvalidArgument, "could not parse provided selectors")
return nil, err
}

resp = new(delegatedidentityv1.FetchJWTSVIDsResponse)
Expand Down
103 changes: 86 additions & 17 deletions pkg/agent/api/delegatedidentity/v1/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,40 @@ func TestSubscribeToX509SVIDs(t *testing.T) {
managerErr error
expectMetrics []fakemetrics.MetricItem
expectResp *delegatedidentityv1.SubscribeToX509SVIDsResponse
req *delegatedidentityv1.SubscribeToX509SVIDsRequest
}{
{
testName: "attest error",
attestErr: errors.New("ohno"),
expectCode: codes.Internal,
expectMsg: "workload attestation failed",
},
{
bleggett marked this conversation as resolved.
Show resolved Hide resolved
testName: "incorrectly populate both pid and selectors",
authSpiffeID: []string{"spiffe://example.org/one"},
identities: []cache.Identity{
identities[0],
},
expectCode: codes.InvalidArgument,
expectMsg: "must provide either selectors or non-zero PID, but not both",
req: &delegatedidentityv1.SubscribeToX509SVIDsRequest{
Selectors: []*types.Selector{{Type: "sa", Value: "foo"}},
Pid: 447,
},
},
{
testName: "incorrectly populate neither pid or selectors",
authSpiffeID: []string{"spiffe://example.org/one"},
identities: []cache.Identity{
identities[0],
},
expectCode: codes.InvalidArgument,
expectMsg: "must provide either selectors or non-zero PID, but not both",
req: &delegatedidentityv1.SubscribeToX509SVIDsRequest{
Selectors: []*types.Selector{},
Pid: 0,
},
},
{
testName: "access to \"privileged\" admin API denied",
authSpiffeID: []string{"spiffe://example.org/one/wrong"},
Expand Down Expand Up @@ -271,22 +298,24 @@ func TestSubscribeToX509SVIDs(t *testing.T) {
ManagerErr: tt.managerErr,
Metrics: metrics,
}
runTest(t, params,
func(ctx context.Context, client delegatedidentityv1.DelegatedIdentityClient) {
selectors := []*types.Selector{{Type: "sa", Value: "foo"}}
req := &delegatedidentityv1.SubscribeToX509SVIDsRequest{
Selectors: selectors,
}

stream, err := client.SubscribeToX509SVIDs(ctx, req)

require.NoError(t, err)
resp, err := stream.Recv()

spiretest.RequireGRPCStatus(t, err, tt.expectCode, tt.expectMsg)
spiretest.RequireProtoEqual(t, tt.expectResp, resp)
require.Equal(t, tt.expectMetrics, metrics.AllMetrics())
})
runTest(t, params, func(ctx context.Context, client delegatedidentityv1.DelegatedIdentityClient) {
req := &delegatedidentityv1.SubscribeToX509SVIDsRequest{
Selectors: []*types.Selector{{Type: "sa", Value: "foo"}},
}
// if test params has a custom request, prefer that
if tt.req != nil {
req = tt.req
}

stream, err := client.SubscribeToX509SVIDs(ctx, req)

require.NoError(t, err)
resp, err := stream.Recv()

spiretest.RequireGRPCStatus(t, err, tt.expectCode, tt.expectMsg)
spiretest.RequireProtoEqual(t, tt.expectResp, resp)
require.Equal(t, tt.expectMetrics, metrics.AllMetrics())
})
})
}
}
Expand Down Expand Up @@ -409,6 +438,7 @@ func TestFetchJWTSVIDs(t *testing.T) {
authSpiffeID []string
audience []string
selectors []*types.Selector
pid int32
expectCode codes.Code
expectMsg string
attestErr error
Expand All @@ -427,6 +457,30 @@ func TestFetchJWTSVIDs(t *testing.T) {
expectCode: codes.Internal,
expectMsg: "workload attestation failed",
},
{
bleggett marked this conversation as resolved.
Show resolved Hide resolved
testName: "incorrectly populate both pid and selectors",
authSpiffeID: []string{"spiffe://example.org/one"},
selectors: []*types.Selector{{Type: "sa", Value: "foo"}},
pid: 447,
audience: []string{"AUDIENCE"},
identities: []cache.Identity{
identities[0],
},
expectCode: codes.InvalidArgument,
expectMsg: "must provide either selectors or non-zero PID, but not both",
},
{
testName: "incorrectly populate neither pid or selectors",
authSpiffeID: []string{"spiffe://example.org/one"},
selectors: []*types.Selector{},
pid: 0,
audience: []string{"AUDIENCE"},
identities: []cache.Identity{
identities[0],
},
expectCode: codes.InvalidArgument,
expectMsg: "must provide either selectors or non-zero PID, but not both",
},
{
testName: "Access to \"privileged\" admin API denied",
authSpiffeID: []string{"spiffe://example.org/one/wrong"},
Expand Down Expand Up @@ -562,6 +616,7 @@ func TestFetchJWTSVIDs(t *testing.T) {
resp, err := client.FetchJWTSVIDs(ctx, &delegatedidentityv1.FetchJWTSVIDsRequest{
Audience: tt.audience,
Selectors: tt.selectors,
Pid: tt.pid,
})

spiretest.RequireGRPCStatus(t, err, tt.expectCode, tt.expectMsg)
Expand All @@ -578,6 +633,7 @@ func TestFetchJWTSVIDs(t *testing.T) {
})
}
}

func TestSubscribeToJWTBundles(t *testing.T) {
ca := testca.New(t, trustDomain1)

Expand Down Expand Up @@ -707,7 +763,11 @@ func runTest(t *testing.T, params testParams, fn func(ctx context.Context, clien
AuthorizedDelegates: params.AuthSpiffeID,
})

service.attestor = FakeAttestor{
service.peerAttestor = FakeAttestor{
err: params.AttestErr,
}

service.delegateWorkloadAttestor = FakeWorkloadPIDAttestor{
err: params.AttestErr,
}

Expand Down Expand Up @@ -735,10 +795,19 @@ type FakeAttestor struct {
err error
}

type FakeWorkloadPIDAttestor struct {
bleggett marked this conversation as resolved.
Show resolved Hide resolved
selectors []*common.Selector
err error
}

func (fa FakeAttestor) Attest(context.Context) ([]*common.Selector, error) {
return fa.selectors, fa.err
}

func (fa FakeWorkloadPIDAttestor) Attest(_ context.Context, _ int) ([]*common.Selector, error) {
return fa.selectors, fa.err
}

type FakeManager struct {
manager.Manager

Expand Down
Loading