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

New flag --namespace-iam-role-key and the ability to request a role at the namespace level #250

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ _testmain.go
/build/
coverage.out
/vendor/
.idea
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

best use a global .gitignore for that

1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func addFlags(s *server.Server, fs *pflag.FlagSet) {
fs.BoolVar(&s.NamespaceRestriction, "namespace-restrictions", false, "Enable namespace restrictions")
fs.StringVar(&s.NamespaceRestrictionFormat, "namespace-restriction-format", s.NamespaceRestrictionFormat, "Namespace Restriction Format (glob/regexp)")
fs.StringVar(&s.NamespaceKey, "namespace-key", s.NamespaceKey, "Namespace annotation key used to retrieve the IAM roles allowed (value in annotation should be json array)")
fs.StringVar(&s.NamespaceIAMRoleKey, "namespace-iam-role-key", s.NamespaceIAMRoleKey, "")
fs.DurationVar(&s.CacheResyncPeriod, "cache-resync-period", s.CacheResyncPeriod, "Kubernetes caches resync period")
fs.StringVar(&s.HostIP, "host-ip", s.HostIP, "IP address of host")
fs.StringVar(&s.NodeName, "node", s.NodeName, "Name of the node where kube2iam is running")
Expand Down
52 changes: 44 additions & 8 deletions mappings/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type RoleMapper struct {
iam *iam.Client
store store
namespaceRestrictionFormat string
namespaceIamRoleKey string
}

type store interface {
Expand All @@ -47,14 +48,24 @@ func (r *RoleMapper) GetRoleMapping(IP string) (*RoleMappingResult, error) {
return nil, err
}

namespace := pod.GetNamespace()
Copy link

@grosser grosser Feb 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make this have 0 overhead for everyone else, how about it only fetches the namespace when the flag is enabled ?


role, err := r.extractRoleARN(pod)
if err != nil {
return nil, err
}

namespaceRole, err := r.extractNamespaceRoleARN(namespace)
if err != nil {
return nil, err
}
if len(namespaceRole) > 0 {
role = namespaceRole
}

// Determine if normalized role is allowed to be used in pod's namespace
if r.checkRoleForNamespace(role, pod.GetNamespace()) {
return &RoleMappingResult{Role: role, Namespace: pod.GetNamespace(), IP: IP}, nil
if r.checkRoleForNamespace(role, namespace) {
return &RoleMappingResult{Role: role, Namespace: namespace, IP: IP}, nil
}

return nil, fmt.Errorf("role requested %s not valid for namespace of pod at %s with namespace %s", role, IP, pod.GetNamespace())
Expand All @@ -73,12 +84,11 @@ func (r *RoleMapper) GetExternalIDMapping(IP string) (string, error) {
return externalID, nil
}

// extractQualifiedRoleName extracts a fully qualified ARN for a given pod,
// extractRoleARN 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
func (r *RoleMapper) extractRoleARN(pod *v1.Pod) (string, error) {
rawRoleName, annotationPresent := pod.GetAnnotations()[r.iamRoleKey]

if !annotationPresent && r.defaultRoleARN == "" {
return "", fmt.Errorf("unable to find role for IP %s", pod.Status.PodIP)
}
Expand All @@ -91,6 +101,23 @@ func (r *RoleMapper) extractRoleARN(pod *v1.Pod) (string, error) {
return r.iam.RoleARN(rawRoleName), nil
}

// extractNamespaceRoleARN extracts a fully qualified ARN for a given namespace,
// if the role is not set on the namespace level we will return empty role arn without error
func (r *RoleMapper) extractNamespaceRoleARN(namespace string) (string, error) {
ns, err := r.store.NamespaceByName(namespace)
if err != nil {
log.Debugf("Unable to find an indexed namespace of %s in order to check if the role iam annotation is present", namespace)
return "", fmt.Errorf("unable to find an indexed namespace of %s", namespace)
}

rawRoleName, annotationPresent := ns.GetAnnotations()[r.iamRoleKey]
if !annotationPresent {
return "", nil
}

return r.iam.RoleARN(rawRoleName), nil
}

// checkRoleForNamespace checks the 'database' for a role allowed in a namespace,
// returns true if the role is found, otheriwse false
func (r *RoleMapper) checkRoleForNamespace(roleArn string, namespace string) bool {
Expand All @@ -100,7 +127,7 @@ func (r *RoleMapper) checkRoleForNamespace(roleArn string, namespace string) boo

ns, err := r.store.NamespaceByName(namespace)
if err != nil {
log.Debug("Unable to find an indexed namespace of %s", namespace)
log.Debugf("Unable to find an indexed namespace of %s", namespace)
return false
}

Expand Down Expand Up @@ -134,7 +161,8 @@ func (r *RoleMapper) DumpDebugInfo() map[string]interface{} {
output := make(map[string]interface{})
rolesByIP := make(map[string]string)
namespacesByIP := make(map[string]string)
rolesByNamespace := make(map[string][]string)
rolesRestrictionsByNamespace := make(map[string][]string)
rolesByNamespace := make(map[string]string)

for _, ip := range r.store.ListPodIPs() {
// When pods have `hostNetwork: true` they share an IP and we receive an error
Expand All @@ -150,18 +178,25 @@ func (r *RoleMapper) DumpDebugInfo() map[string]interface{} {

for _, namespaceName := range r.store.ListNamespaces() {
if namespace, err := r.store.NamespaceByName(namespaceName); err == nil {
rolesByNamespace[namespace.GetName()] = kube2iam.GetNamespaceRoleAnnotation(namespace, r.namespaceKey)
rolesRestrictionsByNamespace[namespace.GetName()] = kube2iam.GetNamespaceRoleAnnotation(namespace, r.namespaceKey)

rawRoleName, annotationPresent := namespace.GetAnnotations()[r.namespaceIamRoleKey]
rolesByNamespace[namespace.GetName()] = ""
if annotationPresent {
rolesByNamespace[namespace.GetName()] = rawRoleName
}
}
}

output["rolesByIP"] = rolesByIP
output["namespaceByIP"] = namespacesByIP
output["rolesByNamespace"] = rolesByNamespace
output["rolesRestrictionsByNamespace"] = rolesRestrictionsByNamespace
return output
}

// NewRoleMapper returns a new RoleMapper for use.
func NewRoleMapper(roleKey string, externalIDKey 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, namespaceRoleKey string) *RoleMapper {
return &RoleMapper{
defaultRoleARN: iamInstance.RoleARN(defaultRole),
iamRoleKey: roleKey,
Expand All @@ -171,5 +206,6 @@ func NewRoleMapper(roleKey string, externalIDKey string, defaultRole string, nam
iam: iamInstance,
store: kubeStore,
namespaceRestrictionFormat: namespaceRestrictionFormat,
namespaceIamRoleKey: namespaceRoleKey,
}
}
79 changes: 79 additions & 0 deletions mappings/mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,84 @@ func TestExtractRoleARN(t *testing.T) {
}
}

func TestExtractNamespaceRoleARN(t *testing.T) {
var roleExtractionTests = []struct {
test string
namespace string
requestedNamespace string
annotations map[string]string
expectedARN string
expectError bool
}{
{
test: "No annotation",
namespace: "default",
requestedNamespace: "default",
annotations: map[string]string{},
expectError: false,
expectedARN: "",
},
{
test: "Namespace is not indexed",
namespace: "default",
requestedNamespace: "default-not-indexed",
annotations: map[string]string{},
expectError: true,
expectedARN: "",
},
{
test: "No annotation at the namespace level",
namespace: "default",
requestedNamespace: "default",
annotations: map[string]string{},
expectError: false,
expectedARN: "",
},
{
test: "Annotation at the namespace level but not the one to request a specific role",
namespace: "default",
requestedNamespace: "default",
annotations: map[string]string{"nonMatchingAnnotation": "something"},
expectError: false,
expectedARN: "",
},
{
test: "Annotation at the namespace level",
namespace: "default",
requestedNamespace: "default",
annotations: map[string]string{roleKey: "arn:aws:iam::999999999999:role/explicit-arn"},
expectError: false,
expectedARN: "arn:aws:iam::999999999999:role/explicit-arn",
},
}
for _, tt := range roleExtractionTests {
t.Run(tt.test, func(t *testing.T) {
rp := RoleMapper{}
rp.iamRoleKey = "roleKey"
rp.iamExternalIDKey = "externalIDKey"
rp.iam = &iam.Client{BaseARN: defaultBaseRole}
rp.store = &storeMock{
namespace: tt.namespace,
annotations: tt.annotations,
}

resp, err := rp.extractNamespaceRoleARN(tt.requestedNamespace)
if tt.expectError && err == nil {
t.Error("Expected error however didn't recieve one")
return
}
if !tt.expectError && err != nil {
t.Errorf("Didn't expect error but recieved %s", err)
return
}
if resp != tt.expectedARN {
t.Errorf("Response [%s] did not equal expected [%s]", resp, tt.expectedARN)
return
}
})
}
}

func TestCheckRoleForNamespace(t *testing.T) {
var roleCheckTests = []struct {
test string
Expand Down Expand Up @@ -371,6 +449,7 @@ func TestCheckRoleForNamespace(t *testing.T) {
annotations: tt.namespaceAnnotations,
},
tt.namespaceRestrictionFormat,
"iam.amazonaws.com/role",
)

resp := rp.checkRoleForNamespace(tt.roleARN, tt.namespace)
Expand Down
5 changes: 4 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
defaultMaxInterval = 1 * time.Second
defaultMetadataAddress = "169.254.169.254"
defaultNamespaceKey = "iam.amazonaws.com/allowed-roles"
defaultNamespaceIAMRoleKey = "iam.amazonaws.com/role"
defaultCacheResyncPeriod = 30 * time.Minute
defaultNamespaceRestrictionFormat = "glob"
healthcheckInterval = 30 * time.Second
Expand All @@ -61,6 +62,7 @@ type Server struct {
HostIP string
NodeName string
NamespaceKey string
NamespaceIAMRoleKey string
CacheResyncPeriod time.Duration
LogLevel string
LogFormat string
Expand Down Expand Up @@ -376,7 +378,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.IAMExternalID, 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, s.NamespaceIAMRoleKey)
log.Debugf("Starting pod and namespace sync jobs with %s resync period", s.CacheResyncPeriod.String())
podSynched := s.k8s.WatchForPods(kube2iam.NewPodHandler(s.IAMRoleKey), s.CacheResyncPeriod)
namespaceSynched := s.k8s.WatchForNamespaces(kube2iam.NewNamespaceHandler(s.NamespaceKey), s.CacheResyncPeriod)
Expand Down Expand Up @@ -438,6 +440,7 @@ func NewServer() *Server {
LogFormat: defaultLogFormat,
MetadataAddress: defaultMetadataAddress,
NamespaceKey: defaultNamespaceKey,
NamespaceIAMRoleKey: defaultNamespaceIAMRoleKey,
CacheResyncPeriod: defaultCacheResyncPeriod,
NamespaceRestrictionFormat: defaultNamespaceRestrictionFormat,
HealthcheckFailReason: "Healthcheck not yet performed",
Expand Down