Skip to content

Commit

Permalink
feat: support IMDSv2 ( use aws-go-sdk-v2 ) (#344)
Browse files Browse the repository at this point in the history
* feat: support IMDSv2 ( use aws-go-sdk-v2 )
* fix: add to readme required ec2:DescribeRegions permission
* fix: GetInstanceIAMRole check error
* fix: GetInstanceId check error
  • Loading branch information
Insidexa authored Nov 19, 2023
1 parent 1290388 commit 0bf7505
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 74 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ so that it can call the EC2 metadata API itself.

### IAM roles

It is necessary to create an IAM role which can assume other roles and assign it to each kubernetes worker.
It is necessary to create an IAM role which can assume other roles and assign it to each kubernetes worker and list regions.
List regions required permissions because aws-go-sdk-v2 doesn't include regions list.

```
{
Expand All @@ -45,7 +46,14 @@ It is necessary to create an IAM role which can assume other roles and assign it
],
"Effect": "Allow",
"Resource": "*"
}
},
{
"Action": [
"ec2:DescribeRegions"
],
"Effect": "Allow",
"Resource": "*"
},
]
}
```
Expand Down
11 changes: 8 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
module github.com/jtblin/kube2iam

go 1.14
go 1.15

require (
github.com/aws/aws-sdk-go v1.8.7
github.com/aws/aws-sdk-go-v2 v1.16.8
github.com/aws/aws-sdk-go-v2/config v1.15.15
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9
github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10
github.com/aws/smithy-go v1.12.0
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a // indirect
github.com/cenk/backoff v1.0.1-0.20160904140958-8edc80b07f38
github.com/coreos/go-iptables v0.1.0
github.com/go-ini/ini v0.0.0-20151119163333-2e44421e256d // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v0.0.0-20160920230813-757bef944d0f
github.com/jmespath/go-jmespath v0.0.0-20151117175822-3433f3ea46d9 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/karlseguin/ccache v2.0.1-0.20160708030345-2f6b517f7bea+incompatible
github.com/karlseguin/expect v1.0.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
Expand Down
29 changes: 29 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/aws/aws-sdk-go v1.8.7 h1:r6KpzKbcDiyHyAxHs/8dtIoS3aqsKo3c66SyUCLwDUE=
github.com/aws/aws-sdk-go v1.8.7/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k=
github.com/aws/aws-sdk-go-v2 v1.16.8 h1:gOe9UPR98XSf7oEJCcojYg+N2/jCRm4DdeIsP85pIyQ=
github.com/aws/aws-sdk-go-v2 v1.16.8/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw=
github.com/aws/aws-sdk-go-v2/config v1.15.15 h1:yBV+J7Au5KZwOIrIYhYkTGJbifZPCkAnCFSvGsF3ui8=
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
github.com/aws/aws-sdk-go-v2/credentials v1.12.10 h1:7gGcMQePejwiKoDWjB9cWnpfVdnz/e5JwJFuT6OrroI=
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9 h1:hz8tc+OW17YqxyFFPSkvfSikbqWcyyHRyPVSTzC0+aI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15 h1:bx5F2mr6H6FC7zNIQoDoUr8wEKnvmwRncujT3FYRtic=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9 h1:5sbyznZC2TeFpa4fvtpvpcGbzeXEEs1l1Jo51ynUNsQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.9/go.mod h1:08tUpeSGN33QKSO7fwxXczNfiwCpbj+GxK6XKwqWVv0=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16 h1:f0ySVcmQhwmzn7zQozd8wBM3yuGBfzdpsOaKQ0/Epzw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.16/go.mod h1:CYmI+7x03jjJih8kBEEFKRQc40UjUokT0k7GbvrhhTc=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1 h1:y88XFO3AJWDVJ3HjcYc+Oo38fB948armdg6ulfphkUM=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.51.1/go.mod h1:bKs78Qpk4syfUFXKhA0hIqT3X0sxmvIAPlEHV4qVbP0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9 h1:sHfDuhbOuuWSIAEDd3pma6p0JgUcR2iePxtCE8gfCxQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.9/go.mod h1:yQowTpvdZkFVuHrLBXmczat4W+WJKg/PafBZnGBLga0=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.13 h1:DQpf+al+aWozOEmVEdml67qkVZ6vdtGUi71BZZWw40k=
github.com/aws/aws-sdk-go-v2/service/sso v1.11.13/go.mod h1:d7ptRksDDgvXaUvxyHZ9SYh+iMDymm94JbVcgvSYSzU=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10 h1:7tquJrhjYz2EsCBvA9VTl+sBAAh1bv7h/sGASdZOGGo=
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0=
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a h1:BtpsbiV638WQZwhA98cEZw2BsbnQJrbd0BI7tsy0W1c=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/cenk/backoff v1.0.1-0.20160904140958-8edc80b07f38 h1:VDgg090yok1SWlSK4hWGMYQLD56iZoVjcoCbdFdvOZ4=
Expand Down Expand Up @@ -56,6 +80,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
Expand All @@ -82,6 +108,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jmespath/go-jmespath v0.0.0-20151117175822-3433f3ea46d9 h1:1SlajWtS+u/6x2Be5vrHyrbSxkeIf/+ISBu//kmjpnc=
github.com/jmespath/go-jmespath v0.0.0-20151117175822-3433f3ea46d9/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand Down
17 changes: 8 additions & 9 deletions iam/arn.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package iam

import (
"context"
"fmt"
"regexp"
"strings"

"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
)

const fullArnPrefix = "arn:"
Expand All @@ -30,19 +31,17 @@ func (iam *Client) RoleARN(role string) string {

// GetBaseArn get the base ARN from metadata service.
func GetBaseArn() (string, error) {
sess, err := session.NewSession()
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return "", err
}
metadata := ec2metadata.New(sess)
if !metadata.Available() {
return "", fmt.Errorf("EC2 Metadata is not available, are you running on EC2?")
}
iamInfo, err := metadata.IAMInfo()

client := imds.NewFromConfig(cfg)
iamInfo, err := client.GetIAMInfo(context.TODO(), &imds.GetIAMInfoInput{})
if err != nil {
return "", err
}
arn := strings.Replace(iamInfo.InstanceProfileArn, "instance-profile", "role", 1)
arn := strings.Replace(iamInfo.IAMInfo.InstanceProfileArn, "instance-profile", "role", 1)
baseArn := strings.Split(arn, "/")
if len(baseArn) < 2 {
return "", fmt.Errorf("can't determine BaseARN")
Expand Down
150 changes: 105 additions & 45 deletions iam/iam.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package iam

import (
"context"
"errors"
"fmt"
"hash/fnv"
"io/ioutil"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/sts"
smithy "github.com/aws/smithy-go"
"github.com/jtblin/kube2iam/metrics"
"github.com/karlseguin/ccache"
)
Expand Down Expand Up @@ -50,24 +52,51 @@ func getHash(text string) string {
return fmt.Sprintf("%x", h.Sum32())
}

// GetInstanceIAMRole get instance IAM role from metadata service.
func GetInstanceIAMRole() (string, error) {
sess, err := session.NewSession()
func getInstanceMetadata(path string) (string, error) {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return "", err
}
metadata := ec2metadata.New(sess)
if !metadata.Available() {
return "", errors.New("EC2 Metadata is not available, are you running on EC2?")

client := imds.NewFromConfig(cfg)
metadataResult, err := client.GetMetadata(context.TODO(), &imds.GetMetadataInput{
Path: path,
})
if err != nil {
return "", errors.New(fmt.Sprintf("EC2 Metadata [%s] response error, got %v", err, path))
}
// https://aws.github.io/aws-sdk-go-v2/docs/making-requests/#responses-with-ioreadcloser
defer metadataResult.Content.Close()
instanceId, err := ioutil.ReadAll(metadataResult.Content)

if err != nil {
return "", errors.New(fmt.Sprintf("Expect to read content [%s] from bytes, got %v", err, path))
}

if string(instanceId) == "" {
return "", errors.New(fmt.Sprintf("EC2 Metadata didn't returned [%s], got empty string", path))
}
iamRole, err := metadata.GetMetadata("iam/security-credentials/")
return string(instanceId), nil
}

// GetInstanceIAMRole get instance IAM role from metadata service.
func GetInstanceIAMRole() (string, error) {
iamRole, err := getInstanceMetadata("iam/security-credentials/")

if err != nil {
return "", err
}
if iamRole == "" || err != nil {
return "", errors.New("EC2 Metadata didn't returned any IAM Role")
return string(iamRole), nil
}

// Get InstanceId for healthcheck
func (iam *Client) GetInstanceId() (string, error) {
instanceId, err := getInstanceMetadata("instance-id")

if err != nil {
return "", err
}
return iamRole, nil
return string(instanceId), nil
}

func sessionName(roleARN, remoteIP string) string {
Expand All @@ -77,10 +106,15 @@ func sessionName(roleARN, remoteIP string) string {
}

// Helper to format IAM return codes for metric labeling
//
// https://aws.github.io/aws-sdk-go-v2/docs/handling-errors/#api-error-responses
// All service API response errors implement the smithy.APIError interface type.
// This interface can be used to handle both modeled or un-modeled service error responses
func getIAMCode(err error) string {
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
return awsErr.Code()
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
return apiErr.ErrorCode()
}
return metrics.IamUnknownFailCode
}
Expand All @@ -97,32 +131,38 @@ func GetEndpointFromRegion(region string) string {
}

// IsValidRegion tests for a vaild region name
func IsValidRegion(promisedLand string) bool {
partitions := endpoints.DefaultResolver().(endpoints.EnumPartitions).Partitions()
for _, p := range partitions {
for region := range p.Regions() {
if promisedLand == region {
return true
}
func IsValidRegion(promisedLand string, regions *ec2.DescribeRegionsOutput) bool {
for _, region := range regions.Regions {
if promisedLand == *region.RegionName {
return true
}
}
return false
}

// EndpointFor implements the endpoints.Resolver interface for use with sts
func (iam *Client) EndpointFor(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
// only for sts service
if service == "sts" {
// only if a valid region is explicitly set
if IsValidRegion(region) {
iam.Endpoint = GetEndpointFromRegion(region)
return endpoints.ResolvedEndpoint{
URL: iam.Endpoint,
SigningRegion: region,
}, nil
// Regions list to validate input region name
//
// https://stackoverflow.com/a/69935735/3945261
func loadRegions() (*ec2.DescribeRegionsOutput, error) {
regionsCache, err := cache.Fetch("awsRegions", time.Hour*24*30, func() (interface{}, error) {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
ec2Client := ec2.NewFromConfig(cfg)
r, err := ec2Client.DescribeRegions(context.TODO(), &ec2.DescribeRegionsInput{})
if err != nil {
return nil, err
}

return r, nil
})

if err != nil {
return nil, err
}
return endpoints.DefaultResolver().EndpointFor(service, region, optFns...)

return regionsCache.Value().(*ec2.DescribeRegionsOutput), nil
}

// AssumeRole returns an IAM role Credentials using AWS STS.
Expand All @@ -140,25 +180,45 @@ func (iam *Client) AssumeRole(roleARN, externalID string, remoteIP string, sessi
timer := metrics.NewFunctionTimer(metrics.IamRequestSec, lvsProducer, nil)
defer timer.ObserveDuration()

sess, err := session.NewSession()
regions, err := loadRegions()
if err != nil {
return nil, err
}
config := aws.NewConfig().WithLogLevel(2)
if iam.UseRegionalEndpoint {
config = config.WithEndpointResolver(iam)

var customSTSResolver = aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
if service == sts.ServiceID && IsValidRegion(region, regions) {
return aws.Endpoint{
URL: GetEndpointFromRegion(region),
SigningRegion: region,
}, nil
}

// returning EndpointNotFoundError will allow the service to fallback to it's default resolution
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})

cfg, err := config.LoadDefaultConfig(
context.TODO(),
config.WithEndpointResolverWithOptions(customSTSResolver),
)
if err != nil {
return nil, err
}
svc := sts.New(sess, config)
svc := sts.NewFromConfig(cfg)
assumeRoleInput := sts.AssumeRoleInput{
DurationSeconds: aws.Int64(int64(sessionTTL.Seconds() * 2)),
DurationSeconds: aws.Int32(int32(sessionTTL.Seconds() * 2)),
RoleArn: aws.String(roleARN),
RoleSessionName: aws.String(sessionName(roleARN, remoteIP)),
}
// Only inject the externalID if one was provided with the request
if externalID != "" {
assumeRoleInput.SetExternalId(externalID)
assumeRoleInput.ExternalId = aws.String(externalID)
}
resp, err := svc.AssumeRole(&assumeRoleInput)

// Maybe use NewAssumeRoleProvider - https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.12.10/credentials/stscreds/assume_role_provider.go#L254
// That's wrapper for AssumeRole with some default values for options
// https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.12.10/credentials/stscreds/assume_role_provider.go#L270
resp, err := svc.AssumeRole(context.TODO(), &assumeRoleInput)
if err != nil {
return nil, err
}
Expand Down
18 changes: 3 additions & 15 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -241,25 +240,14 @@ func (s *Server) doHealthcheck() {
metrics.HealthcheckStatus.Set(healthcheckResult)
}()

resp, err := http.Get(fmt.Sprintf("http://%s/latest/meta-data/instance-id", s.MetadataAddress))
instanceId, err := s.iam.GetInstanceId()
if err != nil {
errMsg = fmt.Sprintf("Error getting instance id %+v", err)
log.Errorf(errMsg)
return
}
if resp.StatusCode != 200 {
errMsg = fmt.Sprintf("Error getting instance id, got status: %+s", resp.Status)
log.Error(errMsg)
return
}
defer resp.Body.Close()
instanceID, err := ioutil.ReadAll(resp.Body)
if err != nil {
errMsg = fmt.Sprintf("Error reading response body %+v", err)
log.Errorf(errMsg)
return
}
s.InstanceID = string(instanceID)

s.InstanceID = string(instanceId)
}

// HealthResponse represents a response for the health check.
Expand Down

0 comments on commit 0bf7505

Please sign in to comment.