Skip to content

Commit

Permalink
Organization List Feature in Server AWS Node Attester Plugin "aws_ii…
Browse files Browse the repository at this point in the history
…d" (#4838)

* Add New Organization Feature

Signed-off-by: Rushikesh Butley <rbutley@confluent.io>
  • Loading branch information
rushi47 committed Apr 18, 2024
1 parent cc37a51 commit 2467fe5
Show file tree
Hide file tree
Showing 9 changed files with 707 additions and 17 deletions.
23 changes: 23 additions & 0 deletions conf/server/server_full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,29 @@ plugins {
# # make sure that the underlying root volume has not been detached
# # prior to attestation. Default: false
# # skip_block_device = false

# # verify_organization: Verify if the attesting node's account ID is part of the configured AWS Organization.
# # Make sure that the IAM role formed from the configuration below (e.g., "arn:aws:iam::management_account_id:role/assume_org_role")
# # can be assumed by the spire-server.
# verify_organization {
# # management_account_id: Management account ID, also known as the root account,
# # value will be the respective organization's management/root account ID. It's a required parameter.
# # management_account_id = ""

# # management_account_region: Management account region, specifies the region
# # in which the management account is hosted. It's an optional parameter.
# # Default: us-west-2
# # management_account_region = ""

# # assume_org_role: Assume org role, specifies the role name present in the management
# # account. It's a required parameter.
# # assume_org_role = ""

# # org_account_map_ttl: Org account map TTL, specifies the interval for retrieving the list of accounts present in the Organization.
# # It's an optional parameter. If specified, it should be greater than or equal to the duration of 1m (minute).
# # Default: 3m.
# # org_account_map_ttl = ""
# }
# }
# }

Expand Down
55 changes: 54 additions & 1 deletion doc/plugin_server_nodeattestor_aws_iid.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ this plugin resolves the agent's AWS IID-based SPIFFE ID into a set of selectors
| `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 |
| `assume_role` | The role to assume | Empty string, Optional parameter. |
| `partition` | The AWS partition SPIRE server is running in &lt;aws&vert;aws-cn&vert;aws-us-gov&gt; | aws |
| `verify_organization` | Verify that nodes belong to a specified AWS Organization [see below](#enabling-aws-node-attestation-organization-validation) | |

A sample configuration:
Sample configuration:

```hcl
NodeAttestor "aws_iid" {
Expand All @@ -45,6 +46,58 @@ In the following configuration,

assuming AWS IID document sent from the spire agent contains `accountId : 12345678`, the spire server will assume "arn:aws:iam::12345678:role/spire-server-delegate" role before making any AWS call for the node attestation. If `assume_role` is configured, the spire server will always assume the role even if the both the spire-server and the spire agent is deployed in the same account.

## Enabling AWS Node Attestation Organization Validation

For configuring AWS Node attestation method with organization validation following configuration can be used:

| Field Name | Description | Constraints |
|----------------------------|-----------------------------------------------------------------------------------------------|--------------------------------------------|
| management_account_id | Account id of the organzation | required |
| management_account_region | Region of management account id | optional |
| assume_org_role | IAM Role name, with capablities to list accounts | required |
| org_account_map_ttl | Cache the list of accounts for particular time. Should be >= 1 minute. Defaults to 3 minute. | optional |

Using the block `verify_organization` the org validation node attestation method will be enabled. With above configuration spire server will form and try to assume the role as: `arn:aws:iam::management_account_id:role/assume_org_role`. When not used, block ex. `verify_organization = {}` should not be empty, it should be completely removed as its optional or should have all required parameters namely `management_account_id`, `assume_org_role`.

The role under: `assume_role` must be created in the management account: `management_account_id`, and it should have a trust relationship with the role assumed by spire server. Below is a sample policy depicting the permissions required along with the trust relationship that needs to be created in management account.

Policy :

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "organizations:ListAccounts",
"Effect": "Allow",
"Resource": "*",
"Sid": "SpireOrganizationListAccountRole"
}
]
}
```

Trust Relationship

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountAssumeRolePolicy",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::account-id-where-spire-is-running:role/spire-control-plane-root-server",
"arn:aws:iam::account-id-where-spire-is-running:role/spire-control-plane-regional-server"
]
},
"Action": "sts:AssumeRole"
}
]
}
```

## 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.
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/spiffe/spire

go 1.22
go 1.22.2

require (
cloud.google.com/go/iam v1.1.7
Expand All @@ -26,6 +26,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ec2 v1.157.0
github.com/aws/aws-sdk-go-v2/service/iam v1.32.0
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0
github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5
Expand Down Expand Up @@ -306,7 +307,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/twmb/murmur3 v1.1.6 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/xanzy/go-gitlab v0.102.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.4 h1:uDj2K47EM1reAY
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.4/go.mod h1:XKCODf4RKHppc96c2EZBGV/oCUC7OClxAo2MEyg4pIk=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 h1:yl7wcqbisxPzknJVfWTLnK83McUvXba+pz2+tPbIUmQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3 h1:CnPWlONzFX9/yO6IGuKg9sWUE8WhKztYRFbhmOHXjJI=
github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3/go.mod h1:hUHSXe9HFEmLfHrXndAX5e69rv0nBsg22VuNQYl0JLM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0 h1:r3o2YsgW9zRcIP3Q0WCmttFVhTuugeKIvT5z9xDspc0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0/go.mod h1:w2E4f8PUfNtyjfL6Iu+mWI96FGttE03z3UdNcUEC4tA=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1 h1:DtKw4TxZT3VrzYupXQJPBqT9ImyobZZE+JIQPPAVxqs=
Expand Down Expand Up @@ -1455,8 +1457,8 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/uber-go/tally/v4 v4.1.16 h1:by2hveWRh/cUReButk6ns1sHK/hiKry7BuOV6iY16XI=
github.com/uber-go/tally/v4 v4.1.16/go.mod h1:RW5DgqsyEPs0lA4b0YNf4zKj7DveKHd73hnO6zVlyW0=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
Expand Down
27 changes: 23 additions & 4 deletions pkg/server/plugin/nodeattestor/awsiid/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
Expand All @@ -18,11 +19,13 @@ var (
type Client interface {
ec2.DescribeInstancesAPIClient
iam.GetInstanceProfileAPIClient
organizations.ListAccountsAPIClient
}

type clientsCache struct {
mtx sync.RWMutex
config *SessionConfig
orgConfig *orgValidationConfig
clients map[string]*cacheEntry
newClient newClientCallback
}
Expand All @@ -32,7 +35,7 @@ type cacheEntry struct {
client Client
}

type newClientCallback func(ctx context.Context, config *SessionConfig, region string, assumeRoleARN string) (Client, error)
type newClientCallback func(ctx context.Context, config *SessionConfig, region string, assumeRoleARN string, orgRoleARN string) (Client, error)

func newClientsCache(newClient newClientCallback) *clientsCache {
return &clientsCache{
Expand All @@ -41,10 +44,11 @@ func newClientsCache(newClient newClientCallback) *clientsCache {
}
}

func (cc *clientsCache) configure(config SessionConfig) {
func (cc *clientsCache) configure(config SessionConfig, orgConfig orgValidationConfig) {
cc.mtx.Lock()
cc.clients = make(map[string]*cacheEntry)
cc.config = &config
cc.orgConfig = &orgConfig
cc.mtx.Unlock()
}

Expand Down Expand Up @@ -81,7 +85,13 @@ func (cc *clientsCache) getClient(ctx context.Context, region, accountID string)
assumeRoleArn = fmt.Sprintf("arn:%s:iam::%s:role/%s", cc.config.Partition, accountID, cc.config.AssumeRole)
}

client, err := cc.newClient(ctx, cc.config, region, assumeRoleArn)
// If organization attestation feature is enabled, assume org role
var orgRoleArn string
if cc.orgConfig.AccountRole != "" {
orgRoleArn = fmt.Sprintf("arn:%s:iam::%s:role/%s", cc.config.Partition, cc.orgConfig.AccountID, cc.orgConfig.AccountRole)
}

client, err := cc.newClient(ctx, cc.config, region, assumeRoleArn, orgRoleArn)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create client: %v", err)
}
Expand All @@ -103,16 +113,25 @@ func (cc *clientsCache) getCachedClient(cacheKey string) *cacheEntry {
return r
}

func newClient(ctx context.Context, config *SessionConfig, region string, assumeRoleARN string) (Client, error) {
func newClient(ctx context.Context, config *SessionConfig, region string, assumeRoleARN string, orgRoleArn string) (Client, error) {
conf, err := newAWSConfig(ctx, config.AccessKeyID, config.SecretAccessKey, region, assumeRoleARN)
if err != nil {
return nil, err
}

// If the orgnizationAttestation feature is enabled, use the role configured for feature.
orgConf, err := newAWSConfig(ctx, config.AccessKeyID, config.SecretAccessKey, region, orgRoleArn)
if err != nil {
return nil, err
}

return struct {
iam.GetInstanceProfileAPIClient
ec2.DescribeInstancesAPIClient
organizations.ListAccountsAPIClient
}{
GetInstanceProfileAPIClient: iam.NewFromConfig(conf),
DescribeInstancesAPIClient: ec2.NewFromConfig(conf),
ListAccountsAPIClient: organizations.NewFromConfig(orgConf),
}, nil
}
90 changes: 83 additions & 7 deletions pkg/server/plugin/nodeattestor/awsiid/iid.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ type IIDAttestorPlugin struct {
mtx sync.RWMutex
clients *clientsCache

orgValidation *orgValidator

// test hooks
hooks struct {
getAWSCACertificate func(string, PublicKeyType) (*x509.Certificate, error)
Expand All @@ -112,12 +114,13 @@ type IIDAttestorPlugin struct {
// IIDAttestorConfig holds hcl configuration for IID attestor plugin
type IIDAttestorConfig struct {
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"`
AssumeRole string `hcl:"assume_role"`
Partition string `hcl:"partition"`
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"`
AssumeRole string `hcl:"assume_role"`
Partition string `hcl:"partition"`
ValidateOrgAccountID *orgValidationConfig `hcl:"verify_organization"`
pathTemplate *agentpathtemplate.Template
trustDomain spiffeid.TrustDomain
getAWSCACertificate func(string, PublicKeyType) (*x509.Certificate, error)
Expand All @@ -126,6 +129,7 @@ type IIDAttestorConfig struct {
// New creates a new IIDAttestorPlugin.
func New() *IIDAttestorPlugin {
p := &IIDAttestorPlugin{}
p.orgValidation = newOrganizationValidationBase(&orgValidationConfig{})
p.clients = newClientsCache(defaultNewClientCallback)
p.hooks.getAWSCACertificate = getAWSCACertificate
p.hooks.getenv = os.Getenv
Expand Down Expand Up @@ -154,6 +158,26 @@ func (p *IIDAttestorPlugin) Attest(stream nodeattestorv1.NodeAttestor_AttestServ
return err
}

// Feature account belongs to organization
// Get the account id of the node from attestation and then check if respective account belongs to organization
if c.ValidateOrgAccountID != nil {
ctxValidateOrg, cancel := context.WithTimeout(stream.Context(), awsTimeout)
defer cancel()
orgClient, err := p.clients.getClient(ctxValidateOrg, c.ValidateOrgAccountID.AccountRegion, c.ValidateOrgAccountID.AccountID)
if err != nil {
return status.Errorf(codes.Internal, "failed to get org client: %v", err)
}

valid, err := p.orgValidation.IsMemberAccount(ctxValidateOrg, orgClient, attestationData.AccountID)
if err != nil {
return status.Errorf(codes.Internal, "failed aws ec2 attestation, issue while verifying if nodes account id: %v belong to org: %v", attestationData.AccountID, err)
}

if !valid {
return status.Errorf(codes.Internal, "failed aws ec2 attestation, nodes account id: %v is not part of configured organization or doesn't have ACTIVE status", attestationData.AccountID)
}
}

inTrustAcctList := false
for _, id := range c.LocalValidAcctIDs {
if attestationData.AccountID == id {
Expand Down Expand Up @@ -272,18 +296,35 @@ func (p *IIDAttestorPlugin) Configure(_ context.Context, req *configv1.Configure
return nil, status.Errorf(codes.InvalidArgument, "invalid partition %q, must be one of: %v", config.Partition, partitions)
}

// Check if Feature flag for account belongs to organization is enabled.
orgConfig := &orgValidationConfig{}
if config.ValidateOrgAccountID != nil {
err = validateOrganizationConfig(config)
if err != nil {
return nil, err
}
orgConfig = config.ValidateOrgAccountID
}

p.mtx.Lock()
defer p.mtx.Unlock()

p.config = config
p.clients.configure(config.SessionConfig)
p.clients.configure(config.SessionConfig, *orgConfig)
if config.ValidateOrgAccountID != nil {
// Setup required config, for validation and for bootstrapping org client
if err := p.orgValidation.configure(orgConfig); err != nil {
return nil, err
}
}

return &configv1.ConfigureResponse{}, nil
}

// SetLogger sets this plugin's logger
func (p *IIDAttestorPlugin) SetLogger(log hclog.Logger) {
p.log = log
p.orgValidation.setLogger(log)
}

func (p *IIDAttestorPlugin) checkBlockDevice(instance ec2types.Instance) error {
Expand Down Expand Up @@ -558,3 +599,38 @@ func isValidAWSPartition(partition string) bool {
}
return false
}

func validateOrganizationConfig(config *IIDAttestorConfig) error {
checkAccID := config.ValidateOrgAccountID.AccountID
checkAccRole := config.ValidateOrgAccountID.AccountRole
checkAccRegion := config.ValidateOrgAccountID.AccountRegion

if checkAccID == "" || checkAccRole == "" {
return status.Errorf(codes.InvalidArgument, "please ensure that %q & %q are present inside block or remove the block: %q for feature node attestation using account id verification", orgAccountID, orgAccountRole, "verify_organization")
}

if checkAccRegion == "" {
config.ValidateOrgAccountID.AccountRegion = orgDefaultAccRegion
}

// check TTL if specified
ttl := orgAccountDefaultListDuration
checkTTL := config.ValidateOrgAccountID.AccountListTTL
if checkTTL != "" {
t, err := time.ParseDuration(checkTTL)
if err != nil {
return status.Errorf(codes.InvalidArgument, "please ensure that %q if configured, it should be in duration and is suffixed with required 'm' for time duration in minute ex. '5m'. Otherwise, remove the: %q, in the block: %q. Default TTL will be: %q", orgAccountListTTL, orgAccountListTTL, "verify_organization", orgAccountDefaultListTTL)
}

if t.Minutes() < orgAccountMinTTL.Minutes() {
return status.Errorf(codes.InvalidArgument, "please ensure that %q if configured, it should be greater than or equal to %q. Otherwise remove the: %q, in the block: %q. Default TTL will be: %q", orgAccountListTTL, orgAccountMinListTTL, orgAccountListTTL, "verify_organization", orgAccountDefaultListTTL)
}

ttl = t
}

// Assign default ttl if ttl doesnt exist.
config.ValidateOrgAccountID.AccountListTTL = ttl.String()

return nil
}
Loading

0 comments on commit 2467fe5

Please sign in to comment.