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

Add support for externalId as an optional annotation #213

Merged
merged 7 commits into from
Jul 29, 2019
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ spec:
### kubernetes annotation

Add an `iam.amazonaws.com/role` annotation to your pods with the role that you want to assume for this pod.
The optional `iam.amazonaws.com/external-id` will allow the use of an ExternalId as part of the assume role

```yaml
apiVersion: v1
Expand All @@ -207,6 +208,7 @@ metadata:
name: aws-cli
annotations:
iam.amazonaws.com/role: role-arn
iam.amazonaws.com/external-id: external-id
spec:
containers:
- image: fstab/aws-cli
Expand Down Expand Up @@ -561,6 +563,7 @@ Usage of kube2iam:
--host-interface string Host interface for proxying AWS metadata (default "docker0")
--host-ip string IP address of host
--iam-role-key string Pod annotation key used to retrieve the IAM role (default "iam.amazonaws.com/role")
--iam-external-id string Pod annotation key used to retrieve the IAM ExternalId (default "iam.amazonaws.com/external-id")
--insecure Kubernetes server should be accessed without verifying the TLS. Testing only
--iptables Add iptables rule (also requires --host-ip)
--log-format string Log format (text/json) (default "text")
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func addFlags(s *server.Server, fs *pflag.FlagSet) {
fs.BoolVar(&s.Debug, "debug", s.Debug, "Enable debug features")
fs.StringVar(&s.DefaultIAMRole, "default-role", s.DefaultIAMRole, "Fallback role to use when annotation is not set")
fs.StringVar(&s.IAMRoleKey, "iam-role-key", s.IAMRoleKey, "Pod annotation key used to retrieve the IAM role")
fs.StringVar(&s.IAMExternalID, "iam-external-id", s.IAMExternalID, "Pod annotation key used to retrieve the IAM ExternalId")
fs.DurationVar(&s.IAMRoleSessionTTL, "iam-role-session-ttl", s.IAMRoleSessionTTL, "TTL for the assume role session")
fs.BoolVar(&s.Insecure, "insecure", false, "Kubernetes server should be accessed without verifying the TLS. Testing only")
fs.StringVar(&s.MetadataAddress, "metadata-addr", s.MetadataAddress, "Address for the ec2 metadata")
Expand Down
11 changes: 8 additions & 3 deletions iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (iam *Client) EndpointFor(service, region string, optFns ...func(*endpoints
}

// AssumeRole returns an IAM role Credentials using AWS STS.
func (iam *Client) AssumeRole(roleARN, remoteIP string, sessionTTL time.Duration) (*Credentials, error) {
func (iam *Client) AssumeRole(roleARN, externalID string, remoteIP string, sessionTTL time.Duration) (*Credentials, error) {
hitCache := true
item, err := cache.Fetch(roleARN, sessionTTL, func() (interface{}, error) {
hitCache = false
Expand All @@ -149,11 +149,16 @@ func (iam *Client) AssumeRole(roleARN, remoteIP string, sessionTTL time.Duration
config = config.WithEndpointResolver(iam)
}
svc := sts.New(sess, config)
resp, err := svc.AssumeRole(&sts.AssumeRoleInput{
assumeRoleInput := sts.AssumeRoleInput{
DurationSeconds: aws.Int64(int64(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)
}
resp, err := svc.AssumeRole(&assumeRoleInput)
if err != nil {
return nil, err
}
Expand Down
17 changes: 16 additions & 1 deletion mappings/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type RoleMapper struct {
defaultRoleARN string
iamRoleKey string
iamExternalIDKey string
namespaceKey string
namespaceRestriction bool
iam *iam.Client
Expand Down Expand Up @@ -59,6 +60,19 @@ func (r *RoleMapper) GetRoleMapping(IP string) (*RoleMappingResult, error) {
return nil, fmt.Errorf("role requested %s not valid for namespace of pod at %s with namespace %s", role, IP, pod.GetNamespace())
}

// GetExternalIDMapping returns the externalID based on IP address
func (r *RoleMapper) GetExternalIDMapping(IP string) (string, error) {
pod, err := r.store.PodByIP(IP)
// If attempting to get a Pod that maps to multiple IPs
if err != nil {
return "", err
}

externalID := pod.GetAnnotations()[r.iamExternalIDKey]

return externalID, nil
}

// extractQualifiedRoleName extracts a fully qualified ARN for a given pod,
// taking into consideration the appropriate fallback logic and defaulting
// logic along with the namespace role restrictions
Expand Down Expand Up @@ -147,10 +161,11 @@ func (r *RoleMapper) DumpDebugInfo() map[string]interface{} {
}

// NewRoleMapper returns a new RoleMapper for use.
func NewRoleMapper(roleKey string, defaultRole string, namespaceRestriction bool, namespaceKey string, iamInstance *iam.Client, kubeStore store, namespaceRestrictionFormat string) *RoleMapper {
func NewRoleMapper(roleKey string, externalIDKey string, defaultRole string, namespaceRestriction bool, namespaceKey string, iamInstance *iam.Client, kubeStore store, namespaceRestrictionFormat string) *RoleMapper {
return &RoleMapper{
defaultRoleARN: iamInstance.RoleARN(defaultRole),
iamRoleKey: roleKey,
iamExternalIDKey: externalIDKey,
namespaceKey: namespaceKey,
namespaceRestriction: namespaceRestriction,
iam: iamInstance,
Expand Down
10 changes: 10 additions & 0 deletions mappings/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
const (
defaultBaseRole = "arn:aws:iam::123456789012:role/"
roleKey = "roleKey"
externalIDKey = "externalIDKey"
namespaceKey = "namespaceKey"
)

Expand Down Expand Up @@ -57,11 +58,18 @@ func TestExtractRoleARN(t *testing.T) {
defaultRole: "explicit-default-role",
expectedARN: "arn:aws:iam::123456789012:role/explicit-default-role",
},
{
test: "Default present, has annotations, has externalID",
annotations: map[string]string{roleKey: "something", externalIDKey: "externalID"},
defaultRole: "explicit-default-role",
expectedARN: "arn:aws:iam::123456789012:role/something",
},
}
for _, tt := range roleExtractionTests {
t.Run(tt.test, func(t *testing.T) {
rp := RoleMapper{}
rp.iamRoleKey = "roleKey"
rp.iamExternalIDKey = "externalIDKey"
Jacobious52 marked this conversation as resolved.
Show resolved Hide resolved
rp.defaultRoleARN = tt.defaultRole
rp.iam = &iam.Client{BaseARN: defaultBaseRole}

Expand Down Expand Up @@ -93,6 +101,7 @@ func TestCheckRoleForNamespace(t *testing.T) {
namespace string
namespaceAnnotations map[string]string
roleARN string
externalID string
namespaceRestrictionFormat string
expectedResult bool
}{
Expand Down Expand Up @@ -352,6 +361,7 @@ func TestCheckRoleForNamespace(t *testing.T) {
t.Run(tt.test, func(t *testing.T) {
rp := NewRoleMapper(
roleKey,
externalIDKey,
tt.defaultArn,
tt.namespaceRestriction,
namespaceKey,
Expand Down
33 changes: 31 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
defaultAppPort = "8181"
defaultCacheSyncAttempts = 10
defaultIAMRoleKey = "iam.amazonaws.com/role"
defaultIAMExternalID = "iam.amazonaws.com/external-id"
defaultLogLevel = "info"
defaultLogFormat = "text"
defaultMaxElapsedTime = 2 * time.Second
Expand All @@ -52,6 +53,7 @@ type Server struct {
BaseRoleARN string
DefaultIAMRole string
IAMRoleKey string
IAMExternalID string
IAMRoleSessionTTL time.Duration
MetadataAddress string
HostInterface string
Expand Down Expand Up @@ -182,6 +184,26 @@ func (s *Server) getRoleMapping(IP string) (*mappings.RoleMappingResult, error)
return roleMapping, nil
}

func (s *Server) getExternalIDMapping(IP string) (string, error) {
var externalID string
var err error
operation := func() error {
externalID, err = s.roleMapper.GetExternalIDMapping(IP)
return err
}

expBackoff := backoff.NewExponentialBackOff()
expBackoff.MaxInterval = s.BackoffMaxInterval
expBackoff.MaxElapsedTime = s.BackoffMaxElapsedTime

err = backoff.Retry(operation, expBackoff)
if err != nil {
return "", err
}

return externalID, nil
}

func (s *Server) beginPollHealthcheck(interval time.Duration) {
if s.healthcheckTicker == nil {
s.doHealthcheck()
Expand Down Expand Up @@ -296,6 +318,12 @@ func (s *Server) roleHandler(logger *log.Entry, w http.ResponseWriter, r *http.R
return
}

externalID, err := s.getExternalIDMapping(remoteIP)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}

roleLogger := logger.WithFields(log.Fields{
"pod.iam.role": roleMapping.Role,
"ns.name": roleMapping.Namespace,
Expand All @@ -311,7 +339,7 @@ func (s *Server) roleHandler(logger *log.Entry, w http.ResponseWriter, r *http.R
return
}

credentials, err := s.iam.AssumeRole(wantedRoleARN, remoteIP, s.IAMRoleSessionTTL)
credentials, err := s.iam.AssumeRole(wantedRoleARN, externalID, remoteIP, s.IAMRoleSessionTTL)
if err != nil {
roleLogger.Errorf("Error assuming role %+v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
Expand Down Expand Up @@ -346,7 +374,7 @@ func (s *Server) Run(host, token, nodeName string, insecure bool) error {
s.k8s = k
s.iam = iam.NewClient(s.BaseRoleARN, s.UseRegionalStsEndpoint)
log.Debugln("Caches have been synced. Proceeding with server.")
s.roleMapper = mappings.NewRoleMapper(s.IAMRoleKey, s.DefaultIAMRole, s.NamespaceRestriction, s.NamespaceKey, s.iam, s.k8s, s.NamespaceRestrictionFormat)
s.roleMapper = mappings.NewRoleMapper(s.IAMRoleKey, s.IAMExternalID, s.DefaultIAMRole, s.NamespaceRestriction, s.NamespaceKey, s.iam, s.k8s, s.NamespaceRestrictionFormat)
podSynched := s.k8s.WatchForPods(kube2iam.NewPodHandler(s.IAMRoleKey))
namespaceSynched := s.k8s.WatchForNamespaces(kube2iam.NewNamespaceHandler(s.NamespaceKey))

Expand Down Expand Up @@ -401,6 +429,7 @@ func NewServer() *Server {
MetricsPort: defaultAppPort,
BackoffMaxElapsedTime: defaultMaxElapsedTime,
IAMRoleKey: defaultIAMRoleKey,
IAMExternalID: defaultIAMExternalID,
BackoffMaxInterval: defaultMaxInterval,
LogLevel: defaultLogLevel,
LogFormat: defaultLogFormat,
Expand Down