Skip to content

Commit

Permalink
feat(templating): adding ability for AccountID substitution in config…
Browse files Browse the repository at this point in the history
…Map (#73)

Allow having $ACCOUNTID variable in the config map if a role exists in
all accounts but has a different ARN per account

rolearn: arn:aws:iam::$ACCOUNTID:role/terraform-role -> rolearn:
arn:aws:iam::123456789:role/terraform-role

---------

Co-authored-by: fmubaidien <faisal.mubaidien@mambu.com>
Co-authored-by: Justinas <12399634+justinas-b@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 22, 2023
1 parent 4b4404a commit 6bcd658
Show file tree
Hide file tree
Showing 14 changed files with 376 additions and 51 deletions.
15 changes: 8 additions & 7 deletions .github/workflows/app-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ jobs:
with:
name: golangci-lint-test-results
path: lint-report.xml
- uses: dorny/test-reporter@v1.6.0
with:
# artifact: golangci-lint-test-results
name: golangci-lint-test-results
path: 'lint-report.xml'
reporter: java-junit
fail-on-error: 'false'
# - name: Test report
# uses: dorny/test-reporter@v1.7.0
# if: success() || failure() # run this step even if previous step failed
# with:
# name: golangci-lint
# path: 'lint-report.xml'
# reporter: java-junit
# fail-on-error: 'false'
test:
name: Go Test
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@
go.work

dist/
build/
.idea/
.env
k8s/
.editorconfig
TODO
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ data:
[]
```
While using this tool, it enables you to deplou `aws-auth` ConfigMap to tool's namespace and provide PermissionSet's name instead of role ARN under `mapRoles` key:
While using this tool, it enables you to deploy `aws-auth` ConfigMap to tool's namespace and provide PermissionSet's name instead of role ARN under `mapRoles` key:

```yaml
apiVersion: v1
Expand All @@ -57,6 +57,46 @@ data:
[]
```

The Tool also allows a placeholder `$ACCOUNTID` in the role ARN, to give a generic role. This is for a specific use case where multiple clusters in multiple accounts use a role of the same name, but the ARN changes because of account ID.

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: aws-iam-authenticator-sso-wrapper
data:
mapAccounts: |
[]
mapRoles: |
- "groups":
- "system:masters"
"rolearn": "arn:aws:iam::$ACCOUNTID:role/generic-role"
"username": "AdminRole:{{SessionName}}"
mapUsers: |
[]
```
The above config map retrieves the AWS account ID and replaces `$ACCOUNTID`

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapAccounts: |
[]
mapRoles: |
- "groups":
- "system:masters"
"rolearn": "arn:aws:iam::123456789:role/generic-role"
"username": "AdminRole:{{SessionName}}"
mapUsers: |
[]
```


The tool will process `aws-auth` ConfigMap from it's local kubernetes namespace and transform it to the format AWS EKS cluster expects. After processing ConfigMap, it's output is saved `kube-system` namespace where PermissionSet's name is translated to corresponding role ARN, meaning `"permissionset": AdminRole"` line will become `"rolearn": "arn:aws:iam::000000000000:role/AWSReservedSSO_AdminRole_0123456789abcdef"`

More details on this problem can found on below issues:
Expand Down Expand Up @@ -99,7 +139,10 @@ Docker image can be obtained from [justinasb/aws-iam-authenticator-sso-wrapper](
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "iam:ListRoles",
"Action": [
"iam:ListRoles",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
]
Expand Down
74 changes: 74 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- mode: Python -*-

PROJECT_NAME = 'aws-iam-authenticator-sso-wrapper'
IMAGE_REGISTRY = 'ttl.sh'
IMAGE_REPO = 'aws-iam-authenticator-sso-wrapper'

# Building go binary locally
local_resource(
name = 'build',
cmd = 'GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -gcflags "all=-N -l" -o ./build/%s .' % PROJECT_NAME,
deps = [ # list of file that will trigger rebuild when modified
'./main.go',
'./aws.go',
'./kubernetes.go',
'./type.go'
],
)

# Set the default registry where image will be pushed to after build and from where it will be pulled in k8s
default_registry('%s/%s' % (IMAGE_REGISTRY, IMAGE_REPO))

# Use custom Dockerfile for Tilt builds, which only takes locally built binary for live reloading.
dockerfile = '''
FROM golang:1.19-alpine
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY ./build/%s /usr/local/bin/%s
''' % (PROJECT_NAME, PROJECT_NAME)

# Wrap a docker_build to restart the given entrypoint after a Live Update.
load('ext://restart_process', 'docker_build_with_restart')
docker_build_with_restart(
ref = PROJECT_NAME,
context = '.',
dockerfile_contents = dockerfile,
entrypoint = '/go/bin/dlv --listen=0.0.0.0:50100 --api-version=2 --headless=true --only-same-user=false --accept-multiclient --check-go-version=false exec /usr/local/bin/%s -- -dst-namespace=%s -dst-configmap=aws-auth-dst -src-configmap=aws-auth-src -interval=1800 -debug' % (PROJECT_NAME, PROJECT_NAME),
only = './build/%s' % PROJECT_NAME, # trigger docker image rebuild only if go binary has been recompiled
live_update = [
# Copy the binary so it gets restarted.
sync(
local_path = PROJECT_NAME,
remote_path = '/usr/local/bin/%s' % PROJECT_NAME,
),
],

)

# Allow the cluster to avoid problems while having kubectl configured to talk to a remote cluster.
allow_k8s_contexts(k8s_context())

# Load .env file
load('ext://dotenv', 'dotenv')
dotenv()

# Provision kubernetes resources
k8s_yaml('tilt-files/namespace.yaml')
k8s_yaml('tilt-files/configMap.yaml')
k8s_yaml('tilt-files/deployment.yaml')
k8s_yaml('tilt-files/roles.yaml')

# Replace IAM role used on SA with one defined in .env file
objects = read_yaml_stream('tilt-files/serviceAccount.yaml')
for o in objects:
o['metadata']['annotations']['eks.amazonaws.com/role-arn'] = os.environ['IAM_ROLE']
k8s_yaml(encode_yaml_stream(objects))

# Configure port-forwarding for delve
k8s_resource(
workload = PROJECT_NAME,
port_forwards = ["50100:50100"], # Set up the K8s port-forward to be able to connect to it locally.
resource_deps = [
'build',
],
)

46 changes: 35 additions & 11 deletions aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,24 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/aws/aws-sdk-go-v2/service/iam/types"
"go.uber.org/zap"
"golang.org/x/exp/slices"
)

// getAWSClient returns an Amazon IAM service client.
// getAWSClientConfig returns an aws.Config to be used on clients.
//
// It initializes the AWS SDK and creates an Amazon IAM service client using the default configuration.
// It takes no parameters and returns a pointer to an iam.Client and an error.
func getAWSClient() (*iam.Client, error) {
// It initializes the AWS SDK and creates an Amazon client configuration.
// It takes no parameters and returns a aws.Config and an error.
func getAWSClientConfig() (aws.Config, error) {
// Initialize AWS SDK
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(defaultAWSRegion))
if err != nil {
return nil, err
return cfg, err
}

// Create an Amazon IAM service client
client := iam.NewFromConfig(cfg)

return client, nil
return cfg, nil
}

// listSSORoles retrieves a list of IAM roles that are used by AWS SSO service.
Expand All @@ -41,11 +39,12 @@ func listSSORoles() ([]types.Role, error) {

logger.Info("Retrieving SSO roles from AWS IAM...")

client, err := getAWSClient()
cfg, err := getAWSClientConfig()
if err != nil {
logger.Fatal("Unable to load SDK config, %v", zap.Error(err))
}

client := iam.NewFromConfig(cfg)

// Create a list roles request
params := &iam.ListRolesInput{
MaxItems: aws.Int32(10),
Expand Down Expand Up @@ -118,3 +117,28 @@ func removePathFromRoleARN(arn string, path string) string {

return r.ReplaceAllString(arn, "/")
}

// Get AWS account ID
func getAccountId() (string, error) {
logger.Debug("Reading AWS Account ID...")

cfg, err := getAWSClientConfig()
if err != nil {
logger.Fatal("Unable to load SDK config, %v", zap.Error(err))
}

client := sts.NewFromConfig(cfg)
if err != nil {
logger.Fatal("Unable to load SDK config, %v", zap.Error(err))
}
input := &sts.GetCallerIdentityInput{}

req, err := client.GetCallerIdentity(context.TODO(), input)
if err != nil {
return "", err
}

logger.Debug(fmt.Sprintf("Retrievied %s as AWS Account ID", *req.Account))

return *req.Account, nil
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ require (
github.com/aws/aws-sdk-go-v2 v1.22.2
github.com/aws/aws-sdk-go-v2/config v1.22.3
github.com/aws/aws-sdk-go-v2/service/iam v1.27.1
github.com/aws/aws-sdk-go-v2/service/sts v1.25.1
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
Expand All @@ -23,7 +25,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.19.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.25.1 // indirect
github.com/aws/smithy-go v1.16.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
Expand Down Expand Up @@ -57,7 +58,6 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
Expand Down
17 changes: 13 additions & 4 deletions kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"

"github.com/aws/aws-sdk-go-v2/service/iam/types"
"go.uber.org/zap"
Expand Down Expand Up @@ -142,7 +143,7 @@ func setConfigMap(clientset kubernetes.Interface, configMapName string, namespac
// - awsIAMRoles: a slice of types.Role structs
//
// It returns a slice of SSORoleMapping structs, where the PermissionSet name is replaced with Role ARN.
func transformRoleMappings(roleMappings []SSORoleMapping, awsIAMRoles []types.Role) []SSORoleMapping {
func transformRoleMappings(roleMappings []SSORoleMapping, awsIAMRoles []types.Role, accountId string) []SSORoleMapping {
// Replace PermissionSet name with Role ARN, if permission
// set is not found - remove it from configMap

Expand All @@ -155,9 +156,17 @@ func transformRoleMappings(roleMappings []SSORoleMapping, awsIAMRoles []types.Ro
// Check if Role Mapping needs translation. If not,
// skip this itteration and add object to updated list
if (roleMapping.PermissionSet == "") || (roleMapping.RoleARN != "") {
logger.Debug("Role Mapping does not need to be translated", zap.Any("roleMapping", roleMapping))
roleMappingsUpdated = append(roleMappingsUpdated, roleMapping)
continue
//Check if rolemapping requires fetching the accountid of the aws account
if(strings.Contains(roleMapping.RoleARN, "$ACCOUNTID")) {
logger.Info("Replacing $ACCOUNTID with Actual account ID")
roleMapping.RoleARN = strings.Replace(roleMapping.RoleARN, "$ACCOUNTID", accountId, -1)
roleMappingsUpdated = append(roleMappingsUpdated, roleMapping)
continue
} else {
logger.Debug("Role Mapping does not need to be translated", zap.Any("roleMapping", roleMapping))
roleMappingsUpdated = append(roleMappingsUpdated, roleMapping)
continue
}
}

// Translate permission set name to ARN
Expand Down
46 changes: 43 additions & 3 deletions kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestTransformRoleMappings(t *testing.T) {
},
}

got := transformRoleMappings(mappings, roles)
got := transformRoleMappings(mappings, roles, "")

if !reflect.DeepEqual(got, want) {
t.Errorf("TransformRoleMappings() returned unexpected object: %+v, want %+v", got, want)
Expand Down Expand Up @@ -275,7 +275,7 @@ func TestTransformRoleMappings(t *testing.T) {
},
}

got := transformRoleMappings(mappings, roles)
got := transformRoleMappings(mappings, roles, "")

if !reflect.DeepEqual(got, want) {
t.Errorf("TransformRoleMappings() returned unexpected object: %+v, want %+v", got, want)
Expand Down Expand Up @@ -327,7 +327,47 @@ func TestTransformRoleMappings(t *testing.T) {
},
}

got := transformRoleMappings(mappings, roles)
got := transformRoleMappings(mappings, roles, "")

if !reflect.DeepEqual(got, want) {
t.Errorf("TransformRoleMappings() returned unexpected object: %+v, want %+v", got, want)
}
})

// Test when ACCOUNTID placeholder was provided, if it is correctly translated
t.Run("Translate ACCOUNTID placeholder actual account ID", func(t *testing.T) {
mappings := []SSORoleMapping{
{
RoleARN: "arn:aws:iam::$ACCOUNTID:role/admin-role",
PermissionSet: "",
Username: "",
Groups: []string{},
},
}

roles := []types.Role{
{
RoleName: aws.String("AWSReservedSSO_devops_0123456789abcdef"),
Path: aws.String("/aws-reserved/sso.amazonaws.com/eu-west-1/"),
Arn: aws.String("arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_devops_0123456789abcdef"),
},
{
RoleName: aws.String("AWSReservedSSO_sre_0123456789abcdef"),
Path: aws.String("/aws-reserved/sso.amazonaws.com/eu-west-1/"),
Arn: aws.String("arn:aws:iam::123456789012:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_sre_0123456789abcdef"),
},
}

want := []SSORoleMapping{
{
RoleARN: "arn:aws:iam::123456789012:role/admin-role",
PermissionSet: "",
Username: "",
Groups: []string{},
},
}

got := transformRoleMappings(mappings, roles, "123456789012")

if !reflect.DeepEqual(got, want) {
t.Errorf("TransformRoleMappings() returned unexpected object: %+v, want %+v", got, want)
Expand Down
Loading

0 comments on commit 6bcd658

Please sign in to comment.