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

Lookup assumed role ARN with IAM API #144

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions cmd/aws-iam-authenticator/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func getConfig() (config.Config, error) {
config := config.Config{
ClusterID: viper.GetString("clusterID"),
ServerEC2DescribeInstancesRoleARN: viper.GetString("server.ec2DescribeInstancesRoleARN"),
ServerIAMGetRoleRoleARN: viper.GetString("server.iamGetRoleRoleARN"),
HostPort: viper.GetInt("server.port"),
Hostname: viper.GetString("server.hostname"),
GenerateKubeconfigPath: viper.GetString("server.generateKubeconfig"),
Expand Down
4 changes: 3 additions & 1 deletion cmd/aws-iam-authenticator/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"os"

"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/aws"
"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/token"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -48,7 +49,8 @@ var verifyCmd = &cobra.Command{
os.Exit(1)
}

id, err := token.NewVerifier(clusterID).Verify(tok)
iamProvider := aws.NewIAMProvider("")
id, err := token.NewVerifier(clusterID, iamProvider).Verify(tok)
if err != nil {
fmt.Fprintf(os.Stderr, "could not verify token: %v\n", err)
os.Exit(1)
Expand Down
12 changes: 9 additions & 3 deletions pkg/arn/arn.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"strings"

"github.com/kubernetes-sigs/aws-iam-authenticator/pkg/aws"

awsarn "github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/endpoints"
)
Expand All @@ -17,7 +19,7 @@ import (
// * IAM role: arn:aws:iam::123456789012:role/S3Access
// * IAM Assumed role: arn:aws:sts::123456789012:assumed-role/Accounting-Role/Mary (converted to IAM role)
// * Federated user: arn:aws:sts::123456789012:federated-user/Bob
func Canonicalize(arn string) (string, error) {
func Canonicalize(arn string, iamProvider aws.IAMProvider) (string, error) {
parsed, err := awsarn.Parse(arn)
if err != nil {
return "", fmt.Errorf("arn '%s' is invalid: '%v'", arn, err)
Expand All @@ -40,8 +42,12 @@ func Canonicalize(arn string) (string, error) {
return "", fmt.Errorf("assumed-role arn '%s' does not have a role", arn)
}
// IAM ARNs can contain paths, part[0] is resource, parts[len(parts)] is the SessionName.
role := strings.Join(parts[1:len(parts)-1], "/")
return fmt.Sprintf("arn:%s:iam::%s:role/%s", parsed.Partition, parsed.AccountID, role), nil
roleName := strings.Join(parts[1:len(parts)-1], "/")
arn, err := iamProvider.GetRoleArn(roleName)
if err != nil {
return "", err
}
return arn, nil
default:
return "", fmt.Errorf("unrecognized resource %s for service sts", parsed.Resource)
}
Expand Down
27 changes: 25 additions & 2 deletions pkg/arn/arn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ import (
"testing"
)

type testIAMProvider struct {
roles map[string]string
}

func (p *testIAMProvider) GetRoleArn(roleName string) (string, error) {
arn, ok := p.roles[roleName]
if !ok {
return "", fmt.Errorf("unknown role")
}
return arn, nil
}

func newTestIAMProvider(roles map[string]string) *testIAMProvider {
return &testIAMProvider{
roles: roles,
}
}

var arnTests = []struct {
arn string // input arn
expected string // canonacalized arn
Expand All @@ -16,12 +34,17 @@ var arnTests = []struct {
{"arn:aws:sts::123456789012:assumed-role/Admin/Session", "arn:aws:iam::123456789012:role/Admin", nil},
{"arn:aws:sts::123456789012:federated-user/Bob", "arn:aws:sts::123456789012:federated-user/Bob", nil},
{"arn:aws:iam::123456789012:root", "arn:aws:iam::123456789012:root", nil},
{"arn:aws:sts::123456789012:assumed-role/Org/Team/Admin/Session", "arn:aws:iam::123456789012:role/Org/Team/Admin", nil},
{"arn:aws:sts::123456789012:assumed-role/WithPath/Session", "arn:aws:iam::123456789012:role/Org/Team/WithPath", nil},
{"arn:aws:sts::123456789012:assumed-role/NotARole/Session", "", fmt.Errorf("unknown role")},
}

func TestUserARN(t *testing.T) {
iamProvider := newTestIAMProvider(map[string]string{
"Admin": "arn:aws:iam::123456789012:role/Admin",
"WithPath": "arn:aws:iam::123456789012:role/Org/Team/WithPath",
})
for _, tc := range arnTests {
actual, err := Canonicalize(tc.arn)
actual, err := Canonicalize(tc.arn, iamProvider)
if err != nil && tc.err == nil || err == nil && tc.err != nil {
t.Errorf("Canoncialize(%s) expected err: %v, actual err: %v", tc.arn, tc.err, err)
continue
Expand Down
157 changes: 157 additions & 0 deletions pkg/aws/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
Copyright 2017 by the contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aws

import (
"errors"
"fmt"
"sync"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"

"github.com/sirupsen/logrus"
)

func newSession(roleARN string) *session.Session {
// Initial credentials loaded from SDK's default credential chain, such as
// the environment, shared credentials (~/.aws/credentials), or EC2 Instance
// Role.

sess := session.Must(session.NewSession())
if aws.StringValue(sess.Config.Region) == "" {
ec2metadata := ec2metadata.New(sess)
regionFound, err := ec2metadata.Region()
if err != nil {
logrus.WithError(err).Fatal("Region not found in shared credentials, environment variable, or instance metadata.")
}
sess.Config.Region = aws.String(regionFound)
}

if roleARN != "" {
logrus.WithFields(logrus.Fields{
"roleARN": roleARN,
}).Infof("Using assumed role for EC2 API")

ap := &stscreds.AssumeRoleProvider{
Client: sts.New(sess),
RoleARN: roleARN,
Duration: time.Duration(60) * time.Minute,
}

sess.Config.Credentials = credentials.NewCredentials(ap)
}
return sess
}

type IAMProvider interface {
// Get a role ARN from role name
GetRoleArn(string) (string, error)
}

type iamProviderImpl struct {
sess *session.Session
}

func NewIAMProvider(roleARN string) IAMProvider {
return &iamProviderImpl{
sess: newSession(roleARN),
}
}

func (p *iamProviderImpl) GetRoleArn(roleName string) (string, error) {
iamService := iam.New(p.sess)
role, err := iamService.GetRole(&iam.GetRoleInput{
RoleName: &roleName,
})
if err != nil {
return "", err
}
return *role.Role.Arn, nil
}

// EC2Provider configures a DNS resolving function for nodes
type EC2Provider interface {
// Get a node name from instance ID
GetPrivateDNSName(string) (string, error)
}

type ec2ProviderImpl struct {
sess *session.Session
privateDNSCache map[string]string
lock sync.Mutex
}

func NewEC2Provider(roleARN string) EC2Provider {
return &ec2ProviderImpl{
sess: newSession(roleARN),
privateDNSCache: make(map[string]string),
}
}

func (p *ec2ProviderImpl) getPrivateDNSNameCache(id string) (string, error) {
p.lock.Lock()
defer p.lock.Unlock()
name, ok := p.privateDNSCache[id]
if ok {
return name, nil
}
return "", errors.New("instance id not found")
}

func (p *ec2ProviderImpl) setPrivateDNSNameCache(id string, privateDNSName string) {
p.lock.Lock()
defer p.lock.Unlock()
p.privateDNSCache[id] = privateDNSName
}

// GetPrivateDNS looks up the private DNS from the EC2 API
func (p *ec2ProviderImpl) GetPrivateDNSName(id string) (string, error) {
privateDNSName, err := p.getPrivateDNSNameCache(id)
if err == nil {
return privateDNSName, nil
}

// Look up instance from EC2 API
instanceIds := []*string{&id}
ec2Service := ec2.New(p.sess)
output, err := ec2Service.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: instanceIds,
})
if err != nil {
return "", fmt.Errorf("failed querying private DNS from EC2 API for node %s: %s", id, err.Error())
}
for _, reservation := range output.Reservations {
for _, instance := range reservation.Instances {
if aws.StringValue(instance.InstanceId) == id {
privateDNSName = aws.StringValue(instance.PrivateDnsName)
p.setPrivateDNSNameCache(id, privateDNSName)
}
}
}
if privateDNSName == "" {
return "", fmt.Errorf("failed to find node %s", id)
}
return privateDNSName, nil
}
6 changes: 6 additions & 0 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ type Config struct {
// running.
ServerEC2DescribeInstancesRoleARN string

// ServerIAMGetRoleRoleARN is an optional AWS Resource Name for an IAM Role to be assumed before calling
// iam:GetRole to determine the role ARN for assumed roles.
// If nil, defaults to using the IAM Role attached to the instance where aws-iam-authenticator is
// running.
ServerIAMGetRoleRoleARN string

// Address defines the hostname or IP Address to bind the HTTPS server to listen to. This is useful when creating
// a local server to handle the authentication request for development.
Address string
Expand Down
Loading