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

Enable agent path template customization for azure_msi node attestor plugin #3488

Merged
Merged
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
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 }}"` |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update server_full.conf, with this new configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!




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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a double take and had to remind myself what happens with the JSON decoder when there are two fields with the same tag. Fortunately the top level field takes precedence over the embedded struct but this is going to be a little subtle if there is any code that tries to use the .Subject field (since it will be unset). Should be easy enough to catch in testing though. I think we're ok here.

Copy link
Contributor Author

@guilhermocc guilhermocc Oct 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about creating a new struct for only storing claims/fields that we want to expose and use. But we would still have another struct being composed by jwt.Claims only, since we use the ValidateWithLeeway method for validation, but I'm not sure if that would make the code clearer since we will be going to deserialize the token two times for two different structs, what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're ok with it as-is. If the Subject field is used at all the unit-tests should catch that it is empty.

}

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