Skip to content

Commit

Permalink
Merge pull request #577 from nnmin-aws/nnmin-rel
Browse files Browse the repository at this point in the history
Add Username Prefix Enforce for DynamicFile mode
  • Loading branch information
k8s-ci-robot committed Mar 7, 2023
2 parents 1070d56 + 68522d7 commit 97bb367
Show file tree
Hide file tree
Showing 14 changed files with 228 additions and 18 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,26 @@ useful if you're migrating from/to EKS and want to keep your mappings, or are
running EKS in addition to some other AWS cluster(s) and want to have the same
mappings in each.

### 5. Set up kubectl to use authentication tokens provided by AWS IAM Authenticator for Kubernetes
#### `DynamicFile`
A local file specified by cfg.dynamicfilepath can serve as the backend. The file
content is expected to be in exactly the same format as the EKSConfigMap. Whenever
this file content changes, authenticator will automatically reload it. This
provides more flexibility on managing the ARN mappings.

Check https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/master/hack/dev/authenticator_with_dynamicfile_mode.yaml
about how to configure the DynamicFile mode.

Run `make e2e RUNNER=kind` to play with a kind cluster with DynamicFile mode enable.
### 5. How to configure reservedPrefixConfig for Kubernetes usernames
The aws-iam-authenticator can support reserved prefix for k8s username. If the reserved prefix is
set, then the username with the reserved prefix will not be authenticated with the error
"username must not begin with with the following prefixes:".

Check https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/master/hack/dev/authenticator_with_dynamicfile_mode.yaml
about how to configure the reserved prefix.


### 6. Set up kubectl to use authentication tokens provided by AWS IAM Authenticator for Kubernetes

> This requires a 1.10+ `kubectl` binary to work. If you receive `Please enter Username:` when trying to use `kubectl` you need to update to the latest `kubectl`
Expand Down
18 changes: 15 additions & 3 deletions cmd/aws-iam-authenticator/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ func getConfig() (config.Config, error) {
EC2DescribeInstancesQps: viper.GetInt("server.ec2DescribeInstancesQps"),
EC2DescribeInstancesBurst: viper.GetInt("server.ec2DescribeInstancesBurst"),
ScrubbedAWSAccounts: viper.GetStringSlice("server.scrubbedAccounts"),
DynamicFilePath: viper.GetString("server.dynamicfilepath"),
DynamicFileUserIDStrict: viper.GetBool("server.dynamicfileUserIDStrict"),
//flags for dynamicfile mode
//DynamicFilePath: the file path containing the roleMapping and userMapping
DynamicFilePath: viper.GetString("server.dynamicfilepath"),
//DynamicFileUserIDStrict: if true, then aws UserId from sts will be used to look up the roleMapping/userMapping; or aws IdentityArn is used
DynamicFileUserIDStrict: viper.GetBool("server.dynamicfileUserIDStrict"),
}
if err := viper.UnmarshalKey("server.mapRoles", &cfg.RoleMappings); err != nil {
return cfg, fmt.Errorf("invalid server role mappings: %v", err)
Expand All @@ -115,7 +118,16 @@ func getConfig() (config.Config, error) {
if err := viper.UnmarshalKey("server.mapAccounts", &cfg.AutoMappedAWSAccounts); err != nil {
logrus.WithError(err).Fatal("invalid server account mappings")
}

var reservedPrefixConfig []config.ReservedPrefixConfig
if err := viper.UnmarshalKey("server.reservedPrefixConfig", &reservedPrefixConfig); err != nil {
return cfg, fmt.Errorf("invalid reserved prefix config: %v", err)
}
if len(reservedPrefixConfig) > 0 {
cfg.ReservedPrefixConfig = make(map[string]config.ReservedPrefixConfig)
for _, c := range reservedPrefixConfig {
cfg.ReservedPrefixConfig[c.BackendMode] = c
}
}
if featureGates.Enabled(config.ConfiguredInitDirectories) {
logrus.Info("ConfiguredInitDirectories feature enabled")
}
Expand Down
13 changes: 13 additions & 0 deletions hack/dev/access-entries-username-prefix.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mapRoles": [
{
"rolearn": "arn:aws:iam::{{AWS_ACCOUNT}}:role/{{USERNAME_TEST_ROLE}}",
"username": "{{SessionName}}:masters",
"groups": [
"system:masters"
],
"userid": "{{USER_ID}}"
}
]
}

8 changes: 8 additions & 0 deletions hack/dev/authenticator_with_dynamicfile_mode.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ server:
kubeconfig: {{AUTHENTICATOR_KUBECONFIG}}
backendmode: [ "MountedFile", "DynamicFile" ]
dynamicfilepath: {{AUTHENTICATOR_DYNAMICFILE_PATH}}
reservedPrefixConfig:
- backendmode: DynamicFile
usernamePrefixReserveList:
- "aws:"
- "iam:"
- "amazon:"
- "system:"
- "eks:"
mapUsers:
- userARN: {{ADMIN_ARN}}
username: kubernetes-admin
Expand Down
81 changes: 77 additions & 4 deletions hack/e2e-dynamicfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ OUTPUT="${OUTPUT:-${REPO_ROOT}/_output}"
# Location of templates, config files, mounts
policies_template="${REPO_ROOT}/hack/dev/policies.template"
access_entry_template="${REPO_ROOT}/hack/dev/access-entries.template"
access_entry_username_prefix_template="${REPO_ROOT}/hack/dev/access-entries-username-prefix.template"
policies_json="${OUTPUT}/dev/authenticator/policies.json"
allow_assume_role_policies_template="${REPO_ROOT}/hack/dev/allow_assume_role_policy.template"
allow_assume_role_policies_json="${OUTPUT}/dev/authenticator/allow_assume_role_policy.json"
Expand All @@ -37,7 +38,7 @@ kubectl_kubeconfig="${client_dir}/kubeconfig.yaml"
REGION=${AWS_REGION:-us-west-2}
AWS_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
AWS_TEST_ROLE=${AWS_TEST_ROLE-authenticator-dev-cluster-testrole}

USERNAME_TEST_ROLE=${USERNAME_TEST_ROLE-authenticator-username-testrole}

function e2e_mountfile() {
sleep 5
Expand All @@ -54,6 +55,78 @@ function e2e_mountfile() {

}

function e2e_dynamicfile_username_prefix_enforce(){
set +e
RoleOutput=$(aws iam get-role --role-name ${USERNAME_TEST_ROLE} 2>/dev/null)

if [ -z "$RoleOutput" ]; then
sed -e "s|{{AWS_ACCOUNT}}|${AWS_ACCOUNT}|g" \
"${policies_template}" > "${policies_json}"
sleep 2
aws iam create-role --role-name ${USERNAME_TEST_ROLE} --assume-role-policy-document file://${policies_json} 1>/dev/null
sleep 10
fi

#detect if run on github and allow the test account to assume role accordingly
if [ $CI = true ]
then
OUT=$(aws iam list-attached-user-policies --user-name awstester)
echo $OUT
if [ -z "$OUT" ]
then
OUT=$(aws iam list-policies --query 'Policies[?PolicyName==`allow-assume-role`]'|jq '.[0]'|jq -r '.Arn')
echo $OUT
if [ -z "$OUT" ]; then
sed -e "s|{{AWS_ACCOUNT}}|${AWS_ACCOUNT}|g" \
"${allow_assume_role_policies_template}" > "${allow_assume_role_policies_json}"
sleep 2
OUT=$(aws iam create-policy --policy-name allow-assume-role --policy-document file://${allow_assume_role_policies_json})
policy_arn=$(echo $OUT| jq -r '.Policy.Arn')
else
policy_arn=$OUT
fi
echo ${policy_arn}
OUT=$(aws iam attach-user-policy --policy-arn ${policy_arn} --user-name awstester)
echo $OUT
echo $(aws iam get-user)
fi
fi

set -e
OUT=$(aws sts assume-role --role-arn arn:aws:iam::${AWS_ACCOUNT}:role/${USERNAME_TEST_ROLE} --role-session-name system);\
export AWS_ACCESS_KEY_ID=$(echo $OUT | jq -r '.Credentials''.AccessKeyId');\
export AWS_SECRET_ACCESS_KEY=$(echo $OUT | jq -r '.Credentials''.SecretAccessKey');\
export AWS_SESSION_TOKEN=$(echo $OUT | jq -r '.Credentials''.SessionToken');

OUT=$(aws sts get-caller-identity|grep "${USERNAME_TEST_ROLE}")
echo "assumed role: "$OUT
if [ -z "$OUT" ]
then
echo "can't assume-role: "${USERNAME_TEST_ROLE}
exit 1
fi
USERID=$(aws sts get-caller-identity|jq -r '.UserId'|cut -d: -f1)
echo "userid: " $USERID

#update access entry to add the test role
sed -e "s|{{AWS_ACCOUNT}}|${AWS_ACCOUNT}|g" \
-e "s|{{USERNAME_TEST_ROLE}}|${USERNAME_TEST_ROLE}|g" \
-e "s|{{USER_ID}}|${USERID}|g" \
"${access_entry_username_prefix_template}" > "${access_entry_tmp}"
mv "${access_entry_tmp}" "${access_entry_json}"
#sleep 10 seconds to make access entry effective
sleep 10
set +e
OUT=$(kubectl --kubeconfig=${kubectl_kubeconfig} --context="test-authenticator" get nodes 2>/var/tmp/err.txt)
if grep -q "Unauthorized" "/var/tmp/err.txt"; then
echo "end to end testing for dynamicfile mode succeeded"
else
echo "end to end testing for dynamicfile mode failed"
exit 1
fi
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
}

function e2e_dynamicfile(){
set +e
RoleOutput=$(aws iam get-role --role-name authenticator-dev-cluster-testrole 2>/dev/null)
Expand Down Expand Up @@ -134,21 +207,21 @@ function e2e_dynamicfile(){
if [ ! -z "$OUT" ]
then
echo $OUT
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
aws iam delete-role --role-name ${AWS_TEST_ROLE}
echo "end to end testing for dynamicfile mode succeeded"
exit 0

else
echo "testing failed"
exit 1
fi
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
}

echo "start end to end testing for mountfile mode"
e2e_mountfile
echo "starting end to end testing for dynamicfile mode"
e2e_dynamicfile
echo "starting end to end testing for dynamicfile mode with username prefix"
e2e_dynamicfile_username_prefix_enforce



Expand Down
15 changes: 12 additions & 3 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,24 @@ type Config struct {
// +optional
Kubeconfig string

// BackendMode is an ordered list of backends to get mappings from. Comma-delimited list of: MountedFile,EKSConfigMap,CRD
// BackendMode is an ordered list of backends to get mappings from. Comma-delimited list of: MountedFile,EKSConfigMap,CRD,DynamicFile
BackendMode []string

// Ec2 DescribeInstances rate limiting variables initially set to defaults until we completely
// understand we don't need to change
EC2DescribeInstancesQps int
EC2DescribeInstancesBurst int
//Dynamic File Path for DynamicFile BackendMode
// Dynamic File Path for DynamicFile BackendMode
DynamicFilePath string
//use UserId for mapping, IdentityArn is not used any more when DynamicFileUserIDStrict=true
// Use UserId for mapping, IdentityArn is not used any more when DynamicFileUserIDStrict=true
DynamicFileUserIDStrict bool
// ReservedPrefixConfig defines reserved username prefixes for each backend
ReservedPrefixConfig map[string]ReservedPrefixConfig
}

type ReservedPrefixConfig struct {
// Backend mode defined in Config.BackendMode
BackendMode string `json:"backendmode" yaml:"backendmode"`
// Defines the reserved prefixes for kubernetes username
UsernamePrefixReserveList []string `json:"usernamePrefixReserveList,omitempty" yaml:"usernamePrefixReserveList,omitempty"`
}
4 changes: 4 additions & 0 deletions pkg/mapper/configmap/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ func (m *ConfigMapMapper) Map(identity *token.Identity) (*config.IdentityMapping
func (m *ConfigMapMapper) IsAccountAllowed(accountID string) bool {
return m.AWSAccount(accountID)
}

func (m *ConfigMapMapper) UsernamePrefixReserveList() []string {
return []string{}
}
4 changes: 4 additions & 0 deletions pkg/mapper/crd/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,7 @@ func (m *CRDMapper) Map(identity *token.Identity) (*config.IdentityMapping, erro
func (m *CRDMapper) IsAccountAllowed(accountID string) bool {
return false
}

func (m *CRDMapper) UsernamePrefixReserveList() []string {
return []string{}
}
7 changes: 4 additions & 3 deletions pkg/mapper/dynamicfile/dynamicfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ type DynamicFileMapStore struct {
users map[string]config.UserMapping
roles map[string]config.RoleMapping
// Used as set.
awsAccounts map[string]interface{}
filename string
userIDStrict bool
awsAccounts map[string]interface{}
filename string
userIDStrict bool
usernamePrefixReserveList []string
}

type DynamicFileData struct {
Expand Down
7 changes: 7 additions & 0 deletions pkg/mapper/dynamicfile/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ func NewDynamicFileMapper(cfg config.Config) (*DynamicFileMapper, error) {
if err != nil {
return nil, err
}
if value, exists := cfg.ReservedPrefixConfig[mapper.ModeDynamicFile]; exists {
ms.usernamePrefixReserveList = value.UsernamePrefixReserveList
}
return &DynamicFileMapper{ms}, nil
}

Expand Down Expand Up @@ -62,3 +65,7 @@ func (m *DynamicFileMapper) Map(identity *token.Identity) (*config.IdentityMappi
func (m *DynamicFileMapper) IsAccountAllowed(accountID string) bool {
return m.AWSAccount(accountID)
}

func (m *DynamicFileMapper) UsernamePrefixReserveList() []string {
return m.usernamePrefixReserveList
}
15 changes: 11 additions & 4 deletions pkg/mapper/file/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (
)

type FileMapper struct {
lowercaseRoleMap map[string]config.RoleMapping
lowercaseUserMap map[string]config.UserMapping
accountMap map[string]bool
lowercaseRoleMap map[string]config.RoleMapping
lowercaseUserMap map[string]config.UserMapping
accountMap map[string]bool
usernamePrefixReserveList []string
}

var _ mapper.Mapper = &FileMapper{}
Expand Down Expand Up @@ -42,7 +43,9 @@ func NewFileMapper(cfg config.Config) (*FileMapper, error) {
for _, m := range cfg.AutoMappedAWSAccounts {
fileMapper.accountMap[m] = true
}

if value, exists := cfg.ReservedPrefixConfig[mapper.ModeMountedFile]; exists {
fileMapper.usernamePrefixReserveList = value.UsernamePrefixReserveList
}
return fileMapper, nil
}

Expand Down Expand Up @@ -90,3 +93,7 @@ func (m *FileMapper) Map(identity *token.Identity) (*config.IdentityMapping, err
func (m *FileMapper) IsAccountAllowed(accountID string) bool {
return m.accountMap[accountID]
}

func (m *FileMapper) UsernamePrefixReserveList() []string {
return m.usernamePrefixReserveList
}
1 change: 1 addition & 0 deletions pkg/mapper/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Mapper interface {
Start(stopCh <-chan struct{}) error
Map(identity *token.Identity) (*config.IdentityMapping, error)
IsAccountAllowed(accountID string) bool
UsernamePrefixReserveList() []string
}

func ValidateBackendMode(modes []string) []error {
Expand Down
12 changes: 12 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,15 @@ func (h *handler) authenticateEndpoint(w http.ResponseWriter, req *http.Request)
})
}

func ReservedPrefixExists(username string, reservedList []string) bool {
for _, prefix := range reservedList {
if len(prefix) > 0 && strings.HasPrefix(username, prefix) {
return true
}
}
return false
}

func (h *handler) doMapping(identity *token.Identity) (string, []string, error) {
var errs []error

Expand All @@ -366,6 +375,9 @@ func (h *handler) doMapping(identity *token.Identity) (string, []string, error)
if err != nil {
return "", nil, fmt.Errorf("mapper %s renderTemplates error: %v", m.Name(), err)
}
if len(m.UsernamePrefixReserveList()) > 0 && ReservedPrefixExists(username, m.UsernamePrefixReserveList()) {
return "", nil, fmt.Errorf("invalid username '%s' for mapper %s: username must not begin with with the following prefixes: %v", username, m.Name(), m.UsernamePrefixReserveList())
}
return username, groups, nil
} else {
if err != mapper.ErrNotMapped {
Expand Down
40 changes: 40 additions & 0 deletions pkg/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,46 @@ func validateMetrics(t *testing.T, opts validateOpts) {
}
}

func TestReservedPrefixExists(t *testing.T) {
cases := []struct {
username string
reservedList []string
want bool
}{
{
"system:masters",
[]string{"aws:", "eks:", "amazon:", "iam:", "system:"},
true,
},
{
"test",
[]string{"aws:", "eks:", "amazon:", "iam:", "system:"},
false,
},
{
"eksb:test",
[]string{"aws:", "eks:", "amazon:", "iam:", "system:"},
false,
},
{
"eks:test",
[]string{"aws:", "eks:", "amazon:", "iam:", "system:"},
true,
},
}
for _, c := range cases {
if got := ReservedPrefixExists(c.username, c.reservedList); got != c.want {
t.Errorf(
"Unexpected result: ReservedPrefixExists(%v,%v): got: %t, wanted %t",
c.username,
c.reservedList,
got,
c.want,
)
}
}
}

func TestAuthenticateNonPostError(t *testing.T) {
resp := httptest.NewRecorder()
req := httptest.NewRequest("GET", "http://k8s.io/authenticate", nil)
Expand Down

0 comments on commit 97bb367

Please sign in to comment.