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
157 changes: 157 additions & 0 deletions pkg/aws/iam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Package aws provides utilities for creating and managing AWS IAM resources
// for Kubernetes workloads running in EKS. It handles the creation of IAM roles,
// policies, and policy attachments that enable pod identity and KMS access.
//
// The package is designed to work with Pulumi and provides a high-level interface
// for managing AWS IAM resources, particularly for services that need to interact
// with AWS KMS for cryptographic operations.
package aws

import (
"encoding/json"
"fmt"

"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

// IAMResources contains AWS IAM resources created for a component.
// These resources work together to provide the necessary permissions
// for a Kubernetes workload to interact with AWS services.
type IAMResources struct {
// Role is the IAM role that can be assumed by the Kubernetes workload
Role *iam.Role
// Policy defines the permissions granted to the role
Policy *iam.Policy
// PolicyAttachment connects the policy to the role
PolicyAttachment *iam.RolePolicyAttachment
}

// CreateIAMResources creates IAM resources (role, policy, and policy attachment) for a component.
// It sets up the necessary permissions for a Kubernetes workload to interact with AWS KMS.
//
// Parameters:
// - ctx: The Pulumi context
// - name: Base name for the IAM resources
// - serviceName: Name of the service that will use these IAM resources
// - keyArn: ARN of the KMS key that the service needs to access
// - parent: Parent Pulumi resource for dependency tracking
//
// Returns:
// - *IAMResources: The created IAM resources
// - error: Any error that occurred during creation
//
// Example:
//
// resources, err := CreateIAMResources(ctx, "my-service", "my-service", keyArn, parent)
// if err != nil {
// return nil, fmt.Errorf("failed to create IAM resources: %w", err)
// }
func CreateIAMResources(
ctx *pulumi.Context,
name string,
serviceName string,
keyArn pulumi.StringInput,
parent pulumi.Resource,
) (*IAMResources, error) {
// Create IAM role with assume role policy for EKS pod identity
assumeRolePolicy := IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{
{
Sid: "AllowEksAuthToAssumeRoleForPodIdentity",
Effect: "Allow",
Principal: struct {
Service []string `json:"Service"`
}{
Service: []string{
"pods.eks.amazonaws.com",
"ec2.amazonaws.com",
},
},
Action: []string{
"sts:AssumeRole",
"sts:TagSession",
},
},
},
}

assumeRolePolicyJSON, err := json.Marshal(assumeRolePolicy)
if err != nil {
return nil, fmt.Errorf("failed to marshal assume role policy: %w", err)
}

role, err := iam.NewRole(ctx, fmt.Sprintf("%s-role", name), &iam.RoleArgs{
AssumeRolePolicy: pulumi.String(assumeRolePolicyJSON),
Description: pulumi.String(fmt.Sprintf("Role for %s pod to assume", serviceName)),
Tags: pulumi.StringMap{
"Name": pulumi.String(fmt.Sprintf("%s-role", name)),
},
}, pulumi.Parent(parent))
if err != nil {
return nil, fmt.Errorf("failed to create IAM role: %w", err)
}

// Create KMS policy for the specified key
policyJSON := CreateKMSPolicy(keyArn)

policy, err := iam.NewPolicy(ctx, fmt.Sprintf("%s-policy", name), &iam.PolicyArgs{
Policy: policyJSON,
}, pulumi.Parent(parent))
if err != nil {
return nil, fmt.Errorf("failed to create IAM policy: %w", err)
}

// Attach the KMS policy to the role
policyAttachment, err := iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-role-policy-attachment", name), &iam.RolePolicyAttachmentArgs{
Role: role.Name,
PolicyArn: policy.Arn,
}, pulumi.Parent(parent))
if err != nil {
return nil, fmt.Errorf("failed to attach policy to role: %w", err)
}

return &IAMResources{
Role: role,
Policy: policy,
PolicyAttachment: policyAttachment,
}, nil
}

// CreateKMSPolicy creates a KMS policy document that grants permissions to sign messages
// and retrieve public keys using the specified KMS key.
//
// Parameters:
// - key: The ARN of the KMS key to create the policy for
//
// Returns:
// - pulumi.StringOutput: A Pulumi output containing the JSON policy document
//
// The policy grants the following permissions:
// - kms:Sign: Allows signing messages using the KMS key
// - kms:GetPublicKey: Allows retrieving the public key associated with the KMS key
func CreateKMSPolicy(key pulumi.StringInput) pulumi.StringOutput {
policy := KMSPolicy{
Version: "2012-10-17",
Statement: []KMSStatement{
{
Effect: "Allow",
Action: []string{
"kms:Sign",
"kms:GetPublicKey",
},
Resource: key,
},
},
}

// Convert to JSON string output
return pulumi.All(key).ApplyT(func(_ []interface{}) (string, error) {
jsonBytes, err := json.Marshal(policy)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}).(pulumi.StringOutput)
}
35 changes: 35 additions & 0 deletions pkg/aws/iam_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package aws provides utilities for creating and managing AWS IAM resources
// for Kubernetes workloads running in EKS. It handles the creation of IAM roles,
// policies, and policy attachments that enable pod identity and KMS access.
package aws

import (
"testing"

"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/stretchr/testify/assert"
)

// TestCreateKMSPolicy tests the creation of KMS policies with different key ARNs.
// Since we can't directly access the string value from StringOutput in a unit test,
// we verify that the function returns non-nil values and that it properly handles
// different input key ARNs.
func TestCreateKMSPolicy(t *testing.T) {
// Test with a simple key ARN
keyArn := "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
key := pulumi.String(keyArn)

// Get the policy string
// Note: We can't directly access the string value from StringOutput in a unit test
// So we'll just verify the function returns a non-nil value and the structure will
// be tested separately when used in the actual builder component.
policy := CreateKMSPolicy(key)

// We can only indirectly test this by asserting the output is not nil
assert.NotNil(t, policy)

// Test with another key to ensure the function uses the provided key
anotherKey := pulumi.String("another-key-arn")
anotherPolicy := CreateKMSPolicy(anotherKey)
assert.NotNil(t, anotherPolicy)
}
54 changes: 54 additions & 0 deletions pkg/aws/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Package aws provides utilities for creating and managing AWS IAM resources
// for Kubernetes workloads running in EKS. It handles the creation of IAM roles,
// policies, and policy attachments that enable pod identity and KMS access.
package aws

import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

// IAMStatement represents a statement in an IAM policy document.
// It defines a single permission statement that specifies what actions
// are allowed or denied on which resources.
type IAMStatement struct {
// Sid is an optional identifier for the statement
Sid string `json:"sid,omitempty"`
// Effect specifies whether the statement allows or denies access
Effect string `json:"effect"`
// Principal specifies who is allowed or denied access
Principal struct {
// Service contains a list of AWS services that can assume this role
Service []string `json:"Service"`
} `json:"Principal"`
// Action specifies the AWS actions that are allowed or denied
Action []string `json:"Action"`
}

// IAMPolicy represents a complete IAM policy document.
// It contains a version and a list of statements that define the policy's permissions.
type IAMPolicy struct {
// Version specifies the policy language version
Version string `json:"Version"`
// Statement contains the list of permission statements
Statement []IAMStatement `json:"Statement"`
}

// KMSStatement represents a statement in a KMS policy document.
// It defines permissions specifically for AWS KMS operations.
type KMSStatement struct {
// Effect specifies whether the statement allows or denies access
Effect string `json:"Effect"`
// Action specifies the KMS actions that are allowed or denied
Action []string `json:"Action"`
// Resource specifies the KMS key ARN that the permissions apply to
Resource pulumi.StringInput `json:"Resource"`
}

// KMSPolicy represents a complete KMS policy document.
// It contains a version and a list of statements that define the KMS permissions.
type KMSPolicy struct {
// Version specifies the policy language version
Version string `json:"Version"`
// Statement contains the list of KMS permission statements
Statement []KMSStatement `json:"Statement"`
}
64 changes: 1 addition & 63 deletions pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
package builder

import (
"encoding/json"
"fmt"
"strconv"

"github.com/init4tech/signet-infra-components/pkg/utils"
"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam"
crd "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
Expand Down Expand Up @@ -53,66 +51,6 @@ func NewBuilder(ctx *pulumi.Context, args BuilderComponentArgs, opts ...pulumi.R
}
component.ServiceAccount = sa

// Create IAM role
assumeRolePolicy := IAMPolicy{
Version: "2012-10-17",
Statement: []IAMStatement{
{
Sid: "AllowEksAuthToAssumeRoleForPodIdentity",
Effect: "Allow",
Principal: struct {
Service []string `json:"Service"`
}{
Service: []string{
"pods.eks.amazonaws.com",
"ec2.amazonaws.com",
},
},
Action: []string{
"sts:AssumeRole",
"sts:TagSession",
},
},
},
}

assumeRolePolicyJSON, err := json.Marshal(assumeRolePolicy)
if err != nil {
return nil, fmt.Errorf("failed to marshal assume role policy: %w", err)
}

role, err := iam.NewRole(ctx, fmt.Sprintf("%s-role", args.Name), &iam.RoleArgs{
AssumeRolePolicy: pulumi.String(assumeRolePolicyJSON),
Description: pulumi.String(fmt.Sprintf("Role for %s pod to assume", args.Name)),
Tags: pulumi.StringMap{
"Name": pulumi.String(fmt.Sprintf("%s-role", args.Name)),
},
}, pulumi.Parent(component))
if err != nil {
return nil, fmt.Errorf("failed to create IAM role: %w", err)
}
component.IAMRole = role

// Create KMS policy
policyJSON := CreateKMSPolicy(args.BuilderEnv.BuilderKey)

policy, err := iam.NewPolicy(ctx, fmt.Sprintf("%s-policy", args.Name), &iam.PolicyArgs{
Policy: policyJSON,
}, pulumi.Parent(component))
if err != nil {
return nil, fmt.Errorf("failed to create IAM policy: %w", err)
}
component.IAMPolicy = policy

// Attach policy to role
_, err = iam.NewRolePolicyAttachment(ctx, fmt.Sprintf("%s-role-policy-attachment", args.Name), &iam.RolePolicyAttachmentArgs{
Role: role.Name,
PolicyArn: policy.Arn,
}, pulumi.Parent(component))
if err != nil {
return nil, fmt.Errorf("failed to attach policy to role: %w", err)
}

// Create ConfigMap for environment variables
configMap, err := utils.CreateConfigMap(
ctx,
Expand Down Expand Up @@ -199,7 +137,7 @@ func NewBuilder(ctx *pulumi.Context, args BuilderComponentArgs, opts ...pulumi.R
},
},
},
}, pulumi.DependsOn([]pulumi.Resource{role, policy}), pulumi.DeleteBeforeReplace(true), pulumi.Parent(component))
}, pulumi.DeleteBeforeReplace(true), pulumi.Parent(component))
if err != nil {
return nil, fmt.Errorf("failed to create deployment: %w", err)
}
Expand Down
33 changes: 0 additions & 33 deletions pkg/builder/helpers.go
Original file line number Diff line number Diff line change
@@ -1,34 +1 @@
package builder

import (
"encoding/json"

"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

// CreateKMSPolicy creates a KMS policy for the builder service.
// Exported for testing.
func CreateKMSPolicy(key pulumi.StringInput) pulumi.StringOutput {
policy := KMSPolicy{
Version: "2012-10-17",
Statement: []KMSStatement{
{
Effect: "Allow",
Action: []string{
"kms:Sign",
"kms:GetPublicKey",
},
Resource: key,
},
},
}

// Convert to JSON string output
return pulumi.All(key).ApplyT(func(_ []interface{}) (string, error) {
jsonBytes, err := json.Marshal(policy)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}).(pulumi.StringOutput)
}
20 changes: 0 additions & 20 deletions pkg/builder/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,6 @@ import (
"github.com/stretchr/testify/assert"
)

func TestCreateKMSPolicy(t *testing.T) {
// Test with a simple key ARN
keyArn := "arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"
key := pulumi.String(keyArn)

// Get the policy string
// Note: We can't directly access the string value from StringOutput in a unit test
// So we'll just verify the function returns a non-nil value and the structure will
// be tested separately when used in the actual builder component.
policy := CreateKMSPolicy(key)

// We can only indirectly test this by asserting the output is not nil
assert.NotNil(t, policy)

// Test with another key to ensure the function uses the provided key
anotherKey := pulumi.String("another-key-arn")
anotherPolicy := CreateKMSPolicy(anotherKey)
assert.NotNil(t, anotherPolicy)
}

func TestBuilderEnvGetEnvMap(t *testing.T) {
// Create a test BuilderEnv with some values
env := BuilderEnv{
Expand Down
Loading
Loading