Skip to content
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
9 changes: 8 additions & 1 deletion doc/plugin_server_nodeattestor_aws_iid.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ this plugin resolves the agent's AWS IID-based SPIFFE ID into a set of selectors
| `access_key_id` | AWS access key id | Value of `AWS_ACCESS_KEY_ID` environment variable |
| `secret_access_key` | AWS secret access key | Value of `AWS_SECRET_ACCESS_KEY` environment variable |
| `skip_block_device` | Skip anti-tampering mechanism which checks to make sure that the underlying root volume has not been detached prior to attestation. | false |
| `disable_instance_profile_selectors` | Disables retrieving the attesting instance profile information that is used in the selectors. Useful in cases where the server cannot reach iam.amazonaws.com | false |

A sample configuration:

Expand All @@ -25,6 +26,12 @@ A sample configuration:
}
}
```

## Disabling Instance Profile Selectors
In cases where spire-server is running in a location with no public internet access available, setting `disable_instance_profile_selectors = true` will prevent the server from making requests to `iam.amazonaws.com`. This is needed as spire-server will fail to attest nodes as it cannot retrieve the metadata information.

When this is enabled, `IAM Role` selector information will no longer be available for use.

## AWS IAM Permissions
The user or role identified by the configured credentials must have permissions for `ec2:DescribeInstances`.

Expand Down Expand Up @@ -58,7 +65,7 @@ This plugin generates the following selectors related to the instance where the

All of the selectors have the type `aws_iid`.

The `IAM role` selector is included in the generated set of selectors only if the instance has an IAM Instance Profile associated.
The `IAM role` selector is included in the generated set of selectors only if the instance has an IAM Instance Profile associated and `disable_instance_profile_selectors = false`

## Security Considerations
The AWS Instance Identity Document, 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
22 changes: 14 additions & 8 deletions pkg/server/plugin/nodeattestor/aws/iid.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,14 @@ type IIDAttestorPlugin struct {

// IIDAttestorConfig holds hcl configuration for IID attestor plugin
type IIDAttestorConfig struct {
SessionConfig `hcl:",squash"`
SkipBlockDevice bool `hcl:"skip_block_device"`
LocalValidAcctIDs []string `hcl:"account_ids_for_local_validation"`
AgentPathTemplate string `hcl:"agent_path_template"`
pathTemplate *template.Template
trustDomain string
awsCaCertPublicKey *rsa.PublicKey
SessionConfig `hcl:",squash"`
SkipBlockDevice bool `hcl:"skip_block_device"`
DisableInstanceProfileSelectors bool `hcl:"disable_instance_profile_selectors"`
LocalValidAcctIDs []string `hcl:"account_ids_for_local_validation"`
AgentPathTemplate string `hcl:"agent_path_template"`
pathTemplate *template.Template
trustDomain string
awsCaCertPublicKey *rsa.PublicKey
}

// New creates a new IIDAttestorPlugin.
Expand Down Expand Up @@ -400,11 +401,16 @@ func (p *IIDAttestorPlugin) resolveSelectors(parent context.Context, instancesDe
}
}

c, err := p.getConfig()
if err != nil {
return nil, err
}

for _, reservation := range instancesDesc.Reservations {
for _, instance := range reservation.Instances {
addSelectors(resolveTags(instance.Tags))
addSelectors(resolveSecurityGroups(instance.SecurityGroups))
if instance.IamInstanceProfile != nil && instance.IamInstanceProfile.Arn != nil {
if !c.DisableInstanceProfileSelectors && instance.IamInstanceProfile != nil && instance.IamInstanceProfile.Arn != nil {
instanceProfileName, err := instanceProfileNameFromArn(*instance.IamInstanceProfile.Arn)
if err != nil {
return nil, err
Expand Down
64 changes: 55 additions & 9 deletions pkg/server/plugin/nodeattestor/aws/iid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,16 @@ func (s *IIDAttestorSuite) TestErrorOnNoSignature() {

func (s *IIDAttestorSuite) TestClientAndIDReturns() {
tests := []struct {
desc string
mockExpect func(mock *mock_aws.MockClient)
expectID string
expectSelectors []*common.Selector
expectErr string
replacementTemplate string
allowList []string
skipBlockDev bool
skipEC2Block bool
desc string
mockExpect func(mock *mock_aws.MockClient)
expectID string
expectSelectors []*common.Selector
expectErr string
replacementTemplate string
allowList []string
skipBlockDev bool
skipEC2Block bool
disableInstanceProfileSelectors bool
}{
{
desc: "error on call",
Expand Down Expand Up @@ -409,6 +410,47 @@ func (s *IIDAttestorSuite) TestClientAndIDReturns() {
replacementTemplate: "{{ .PluginName}}/zone1/{{ .Tags.Hostname }}",
expectID: "spiffe://example.org/spire/agent/aws_iid/zone1/%3Cno%20value%3E",
},
{
desc: "success, ignore instance profile selectors",
disableInstanceProfileSelectors: true,
mockExpect: func(mock *mock_aws.MockClient) {
output := getDefaultDescribeInstancesOutput()
output.Reservations[0].Instances[0].Tags = []*ec2.Tag{
{
Key: aws.String("Hostname"),
Value: aws.String("host1"),
},
}
output.Reservations[0].Instances[0].SecurityGroups = []*ec2.GroupIdentifier{
{
GroupId: aws.String("TestGroup"),
GroupName: aws.String("Test Group Name"),
},
}
output.Reservations[0].Instances[0].IamInstanceProfile = &ec2.IamInstanceProfile{
Arn: aws.String("arn:aws::::instance-profile/" + testProfile),
}
output.Reservations[0].Instances[0].RootDeviceType = &instanceStoreType
output.Reservations[0].Instances[0].NetworkInterfaces[0].Attachment.DeviceIndex = &zeroDeviceIndex
setAttestExpectations(mock, output, nil)
gipo := &iam.GetInstanceProfileOutput{
InstanceProfile: &iam.InstanceProfile{
Roles: []*iam.Role{
{Arn: aws.String("role1")},
{Arn: aws.String("role2")},
},
},
}
setResolveSelectorsExpectations(mock, gipo)
},
replacementTemplate: "{{ .PluginName}}/zone1/{{ .Tags.Hostname }}",
expectSelectors: []*common.Selector{
{Type: caws.PluginName, Value: "sg:id:TestGroup"},
{Type: caws.PluginName, Value: "sg:name:Test Group Name"},
{Type: caws.PluginName, Value: "tag:Hostname:host1"},
},
expectID: "spiffe://example.org/spire/agent/aws_iid/zone1/host1",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -446,6 +488,10 @@ func (s *IIDAttestorSuite) TestClientAndIDReturns() {
configStr += "\nskip_ec2_attest_calling = true"
}

if tt.disableInstanceProfileSelectors {
configStr += "\ndisable_instance_profile_selectors = true"
}

_, err := s.p.Configure(context.Background(), &plugin.ConfigureRequest{
Configuration: configStr,
GlobalConfig: &plugin.ConfigureRequest_GlobalConfig{TrustDomain: "example.org"},
Expand Down