Skip to content

Commit

Permalink
Enable agent path template customization for azure_msi node attestor …
Browse files Browse the repository at this point in the history
…plugin (spiffe#3488)

Signed-off-by: Guilherme Carvalho <guilhermbrsp@gmail.com>
  • Loading branch information
guilhermocc authored and stevend-uber committed Oct 13, 2023
1 parent e1be1fa commit 6890072
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 71 deletions.
11 changes: 5 additions & 6 deletions conf/server/server_full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ server {
# domain as the server and need not have a corresponding admin registration
# entry with the server.
# admin_ids = ["spiffe://example.org/my/admin"]

# bind_address: IP address or DNS name of the SPIRE server.
# Default: 0.0.0.0.
bind_address = "127.0.0.1"
Expand Down Expand Up @@ -145,7 +145,7 @@ server {

# default_svid_ttl: The default SVID TTL. Default: 1h.
# default_svid_ttl = "1h"

# omit_x509svid_uid: If true, the subject on X509-SVIDs will not contain
# the unique ID attribute. This configurable is deprecated and will be
# removed from a future release.
Expand Down Expand Up @@ -329,10 +329,9 @@ plugins {
# # app_secret = ""
# # }
# # }
# }

# # }
# # }
# # agent_path_template: A URL path portion format of Agent's SPIFFE ID.
# # Describe in text/template format.
# # agent_path_template = ""
# }
# }

Expand Down
32 changes: 23 additions & 9 deletions doc/plugin_server_nodeattestor_azure_msi.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ attestation or to resolve selectors.

## Configuration

| Configuration | Required | Description | Default |
| --------------- | ----------- | ----------------------- |
| `tenants` | Required | A map of tenants, keyed by tenant ID, that are authorized for attestation. Tokens for unspecified tenants are rejected. | |
| Configuration | Required | Description | Default |
|-----------------------|----------|-------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| `tenants` | Required | A map of tenants, keyed by tenant ID, that are authorized for attestation. Tokens for unspecified tenants are rejected. | |
| `agent_path_template` | Optional | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `"/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}"` |



Each tenant in the main configuration supports the following

| Configuration | Required | Description | Default |
| ----------------- | ----------- | ----------------------- |
| Configuration | Required | Description | Default |
|-------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------|
| `resource_id` | Optional | The resource ID (or audience) for the tenant's MSI token. Tokens for a different resource ID are rejected | https://management.azure.com/ |
| `use_msi` | [Optional](#authenticating-to-azure) | Whether or not to use MSI to authenticate to Azure services for selector resolution. | false |
| `subscription_id` | [Optional](#authenticating-to-azure) | The subscription the tenant resides in | |
| `app_id` | [Optional](#authenticating-to-azure) | The application id | |
| `app_secret` | [Optional](#authenticating-to-azure) | The application secret | |
| `use_msi` | [Optional](#authenticating-to-azure) | Whether or not to use MSI to authenticate to Azure services for selector resolution. | false |
| `subscription_id` | [Optional](#authenticating-to-azure) | The subscription the tenant resides in | |
| `app_id` | [Optional](#authenticating-to-azure) | The application id | |
| `app_secret` | [Optional](#authenticating-to-azure) | The application secret | |

It is important to note that the resource ID MUST be for a well known Azure
service, or an app ID for a registered app in Azure AD. Azure will not issue an
Expand Down Expand Up @@ -97,6 +99,18 @@ The plugin produces the following selectors.

All of the selectors have the type `azure_msi`.

## Agent Path Template
The agent path template is a way of customizing the format of generated SPIFFE IDs for agents.
The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [MSI access token](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#payload-claims).

Some useful values are:

| Value | Description |
|-----------------------|------------------------------------------------------------|
| .PluginName | The name of the plugin |
| .TenantID | Azure tenant identifier |
| .PrincipalID | A identifier that is unique to a particular application ID |

## Security Considerations
The Azure Managed Service Identity token, which this attestor leverages to prove node identity, is available to any process running on the node by default. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run.

Expand Down
43 changes: 30 additions & 13 deletions doc/plugin_server_nodeattestor_gcp_iit.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ This plugin requires an allow list of ProjectID from which nodes can be attested

## Configuration

| Configuration | Description | Default |
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|---------|
| `projectid_allow_list` | List of ProjectIDs from which nodes can be attested. | |
| `use_instance_metadata` | If true, instance metadata is fetched from the Google Compute Engine API and used to augment the node selectors produced by the plugin. | false |
| `service_account_file` | Path to the service account file used to authenticate with the Google Compute Engine API | |
| `allowed_label_keys` | Instance label keys considered for selectors | |
| `allowed_metadata_keys` | Instance metadata keys considered for selectors | |
| `max_metadata_value_size` | Sets the maximum metadata value size considered by the plugin for selectors | 128 |
| Configuration | Description | Default |
|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| `projectid_allow_list` | List of ProjectIDs from which nodes can be attested. | |
| `use_instance_metadata` | If true, instance metadata is fetched from the Google Compute Engine API and used to augment the node selectors produced by the plugin. | false |
| `service_account_file` | Path to the service account file used to authenticate with the Google Compute Engine API | |
| `allowed_label_keys` | Instance label keys considered for selectors | |
| `allowed_metadata_keys` | Instance metadata keys considered for selectors | |
| `max_metadata_value_size` | Sets the maximum metadata value size considered by the plugin for selectors | 128 |
| `agent_path_template` | A URL path portion format of Agent's SPIFFE ID. Describe in text/template format. | `"/{{ .PluginName }}/{{ .ProjectID }}/{{ .InstanceID }}"` |

A sample configuration:

Expand All @@ -31,11 +32,11 @@ A sample configuration:

This plugin generates the following selectors based on information contained in the Instance Identity Token:

| Selector | Example | Description |
| -------------------------- | ------------------------------------------------------------ | ----------------------------------------- |
| `gcp_iit:project-id` | `gcp_iit:project-id:big-kahuna-123456` | ID of the project containing the instance |
| `gcp_iit:zone` | `gcp_iit:zone:us-west1-b` | Zone containing the instance |
| `gcp_iit:instance-name` | `gcp_iit:instance-name:blog-server` | Name of the instance |
| Selector | Example | Description |
|-------------------------|----------------------------------------|-------------------------------------------|
| `gcp_iit:project-id` | `gcp_iit:project-id:big-kahuna-123456` | ID of the project containing the instance |
| `gcp_iit:zone` | `gcp_iit:zone:us-west1-b` | Zone containing the instance |
| `gcp_iit:instance-name` | `gcp_iit:instance-name:blog-server` | Name of the instance |

If `use_instance_metadata` is true, then the Google Compute Engine API is queried for instance metadata which is used to populate these additional selectors:

Expand Down Expand Up @@ -67,6 +68,22 @@ The plugin uses the Application Default Credentials to authenticate with the Goo
The service account must have IAM permissions and Authorization Scopes granting access to the following APIs:
* [compute.instances.get](https://cloud.google.com/compute/docs/reference/rest/v1/instances/get)

## Agent Path Template
The agent path template is a way of customizing the format of generated SPIFFE IDs for agents.
The template formatter is using Golang text/template conventions, it can reference values provided by the plugin or in a [Compute Engine identity token](https://cloud.google.com/compute/docs/instances/verifying-instance-identity#payload).

Some useful values are:

| Value | Description |
|----------------------------|------------------------------------------------------------------|
| .PluginName | The name of the plugin |
| .ProjectID | The ID for the project where the instance was created |
| .InstanceID | The unique ID for the instance to which this token belongs. |
| .ProjectNumber | The unique number for the project where you created the instance |
| .Zone | The zone where the instance is located |
| .InstanceCreationTimestamp | A Unix timestamp indicating when you created the instance. |


## Security Considerations
The Instance Identity Token, which this attestor leverages to prove node identity, is available to any process running on the node by default. As a result, it is possible for non-agent code running on a node to attest to the SPIRE Server, allowing it to obtain any workload identity that the node is authorized to run.

Expand Down
38 changes: 26 additions & 12 deletions pkg/common/plugin/azure/msi.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"encoding/json"
"io"
"net/http"
"net/url"
"path"

"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/spire/pkg/common/agentpathtemplate"
"github.com/spiffe/spire/pkg/common/idutil"
"github.com/zeebo/errs"
"gopkg.in/square/go-jose.v2/jwt"
)
Expand All @@ -17,8 +18,12 @@ const (
// audience of the MSI token. The current value is the service ID for the
// Resource Manager API.
DefaultMSIResourceID = "https://management.azure.com/"
PluginName = "azure_msi"
)

// DefaultAgentPathTemplate is the default text/template
var DefaultAgentPathTemplate = agentpathtemplate.MustParse("/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}")

type ComputeMetadata struct {
Name string `json:"name"`
SubscriptionID string `json:"subscriptionId"`
Expand All @@ -35,16 +40,8 @@ type MSIAttestationData struct {

type MSITokenClaims struct {
jwt.Claims
TenantID string `json:"tid,omitempty"`
}

func (c *MSITokenClaims) AgentID(trustDomain string) string {
u := url.URL{
Scheme: "spiffe",
Host: trustDomain,
Path: path.Join("spire", "agent", "azure_msi", c.TenantID, c.Subject),
}
return u.String()
TenantID string `json:"tid,omitempty"`
PrincipalID string `json:"sub,omitempty"`
}

type HTTPClient interface {
Expand Down Expand Up @@ -125,6 +122,23 @@ func FetchInstanceMetadata(ctx context.Context, cl HTTPClient) (*InstanceMetadat
return metadata, nil
}

type agentPathTemplateData struct {
MSITokenClaims
PluginName string
}

func MakeAgentID(td spiffeid.TrustDomain, agentPathTemplate *agentpathtemplate.Template, claims *MSITokenClaims) (spiffeid.ID, error) {
agentPath, err := agentPathTemplate.Execute(agentPathTemplateData{
MSITokenClaims: *claims,
PluginName: PluginName,
})
if err != nil {
return spiffeid.ID{}, err
}

return idutil.AgentID(td, agentPath)
}

func tryRead(r io.Reader) string {
b := make([]byte, 1024)
n, _ := r.Read(b)
Expand Down
83 changes: 73 additions & 10 deletions pkg/common/plugin/azure/msi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@ package azure

import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"

"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/spiffe/spire/pkg/common/agentpathtemplate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2/jwt"
)

func TestMSITokenClaims(t *testing.T) {
claims := MSITokenClaims{
Claims: jwt.Claims{
Subject: "PRINCIPALID",
},
TenantID: "TENANTID",
}
require.Equal(t, "spiffe://example.org/spire/agent/azure_msi/TENANTID/PRINCIPALID", claims.AgentID("example.org"))
}

func TestFetchMSIToken(t *testing.T) {
ctx := context.Background()

Expand Down Expand Up @@ -114,6 +108,75 @@ func TestFetchInstanceMetadata(t *testing.T) {
require.Equal(t, expected, metadata)
}

func TestMakeAgentID(t *testing.T) {
type args struct {
td string
agentPathTemplate string
claims *MSITokenClaims
}
tests := []struct {
name string
args args
want string
errWanted error
}{
{
name: "successfully applies template",
args: args{
td: "example.org",
agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}",
claims: &MSITokenClaims{
Claims: jwt.Claims{},
TenantID: "TENANTID",
PrincipalID: "PRINCIPALID",
},
},
want: "spiffe://example.org/spire/agent/azure_msi/TENANTID/PRINCIPALID",
errWanted: nil,
},
{
name: "error applying template with non-existent field",
args: args{
td: "example.org",
agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .NonExistent }}",
claims: &MSITokenClaims{
Claims: jwt.Claims{},
TenantID: "TENANTID",
PrincipalID: "PRINCIPALID",
},
},
want: "",
errWanted: errors.New("template: agent-path:1:38: executing \"agent-path\" at <.NonExistent>: can't evaluate field NonExistent in type azure.agentPathTemplateData"),
},
{
name: "error building agent ID with invalid path",
args: args{
td: "example.org",
agentPathTemplate: "/{{ .PluginName }}/{{ .TenantID }}/{{ .PrincipalID }}",
claims: &MSITokenClaims{
Claims: jwt.Claims{},
},
},
want: "",
errWanted: errors.New("invalid agent path suffix \"/azure_msi//\": path cannot contain empty segments"),
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
td := spiffeid.RequireTrustDomainFromString(test.args.td)
agentPathTemplate, _ := agentpathtemplate.Parse(test.args.agentPathTemplate)
got, err := MakeAgentID(td, agentPathTemplate, test.args.claims)
if test.errWanted != nil {
require.EqualError(t, err, test.errWanted.Error())
return
}
assert.NoError(t, err)
assert.Equal(t, test.want, got.String())
})
}
}

func fakeTokenHTTPClient(statusCode int, body string) HTTPClient {
return HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
// assert the expected request values
Expand Down
Loading

0 comments on commit 6890072

Please sign in to comment.