Skip to content

Commit

Permalink
Added AWS_STS_REGIONAL_ENDPOINTS flag/annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
micahhausler committed Apr 26, 2020
1 parent 7362bae commit 454d3b4
Show file tree
Hide file tree
Showing 24 changed files with 752 additions and 77 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ deploy/deployment.yaml
build
/certs/
SAMToolkit.*
coverage.out
143 changes: 139 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ This webhook is for mutating pods that will require AWS IAM access.
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85"
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"__doc_comment": "scope the role to the service account (optional)",
"StringEquals": {
"oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85:sub": "system:serviceaccount:default:my-serviceaccount"
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:my-serviceaccount"
},
"__doc_comment": "scope the role to a namespace (optional)",
"StringLike": {
"oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85:sub": "system:serviceaccount:default:*"
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:*"
}
}
}
Expand All @@ -46,6 +46,10 @@ This webhook is for mutating pods that will require AWS IAM access.
namespace: default
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::111122223333:role/s3-reader"
# optional. Defaults to "sts.amazonaws.com" if not set
eks.amazonaws.com/audience: "sts.amazonaws.com"
# optional. Defaults to omiting injection if unset. A value of "legacy" prevents the environment variable even if the flag is set
eks.amazonaws.com/sts-regional-endpoints: "regional"
```
4. All new pod pods launched using this Service Account will be modified to use
IAM for pods. Below is an example pod spec with the environment variables and
Expand All @@ -71,6 +75,8 @@ This webhook is for mutating pods that will require AWS IAM access.
value: "arn:aws:iam::111122223333:role/s3-reader"
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
- name: AWS_STS_REGIONAL_ENDPOINTS
value: "regional"
volumeMounts:
- mountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount/"
name: aws-token
Expand All @@ -83,7 +89,7 @@ This webhook is for mutating pods that will require AWS IAM access.
expirationSeconds: 86400
path: token
```

### Usage with Windows container workloads

To ensure workloads are scheduled on windows nodes have the right environment variables, they must have a `nodeSelector` targeting windows it must run on. Workloads targeting windows nodes using `nodeAffinity` are currently not supported.
Expand Down Expand Up @@ -119,12 +125,14 @@ Usage of amazon-eks-pod-identity-webhook:
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--metrics-port int Port to listen on for metrics and healthz (http) (default 9999)
--namespace string (in-cluster) The namespace name this webhook and the tls secret resides in (default "eks")
--port int Port to listen on (default 443)
--service-name string (in-cluster) The service name fronting this webhook (default "pod-identity-webhook")
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when openning log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
--sts-regional-endpoint string Value to inject for the AWS_STS_REGIONAL_ENDPOINTS env var in mutated pods. Defaults to no value/injection. Must be "regional" or ""
--tls-cert string (out-of-cluster) TLS certificate file path (default "/etc/webhook/certs/tls.cert")
--tls-key string (out-of-cluster) TLS key file path (default "/etc/webhook/certs/tls.key")
--tls-secret string (in-cluster) The secret name for storing the TLS serving cert (default "pod-identity-webhook")
Expand All @@ -140,6 +148,21 @@ Usage of amazon-eks-pod-identity-webhook:
When the `aws-default-region` flag is set this webhook will inject `AWS_DEFAULT_REGION` and `AWS_REGION` in mutated containers if `AWS_DEFAULT_REGION` and `AWS_REGION` are not already set.
### AWS_STS_REGIONAL_ENDPOINTS Injection
When the `sts-regional-endpoint` flag is set to `regional`, the webhook will
inject the environment variable `AWS_STS_REGIONAL_ENDPOINTS` with the value set
to `regional`. This environment variable will configure the AWS SDKs to perform
the `sts:AssumeRoleWithWebIdentity` call to get credentials from the regional
endpoint, instead of the global endpoint in `us-east-1`. This is desirable in
almost all cases, unless the STS regional endpoint is [disabled in your
account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html).
An empty value will not inject the variable in mutated pods.
You can also enable this per-service account with the annotation
`eks.amazonaws.com/sts-regional-endpoint` set to `regional`. Setting the
annotation value to `legacy` will prevent the environment variable from being
added for associated pods even if the webhook flag is configured to `regional`.
## Installation
Expand All @@ -164,6 +187,118 @@ TODO
## Development
TODO
## Guides
### Different roles in the same pod
You can use different IAM roles in different containers within the same pod, but
there is additional setup and configuration that you'll have to modify your pod
spec by adding the volumes and environment variables without the help of this
webhook.
The way this works is by using different client IDs (also known as audience) in
different tokens. Below is an example pod, but you'll probably use this on a
Deployment, Daemonset, or some other controller type.
First, you'll need to modify your IAM OIDC Identity Provider (IDP) to add an
additional audience/client ID. In this example, the IDP will use the audiences
`sts.amazonaws.com` and `some-other-client-id-arbitrary-string`.
```yaml
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: default
spec:
serviceAccountName: my-serviceaccount
containers:
- name: container1
image: container-image1
env:
- name: AWS_ROLE_ARN
value: "arn:aws:iam::111122223333:role/role-1"
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
volumeMounts:
- mountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount/"
name: aws-token-1
- name: container2
image: container-image2
env:
- name: AWS_ROLE_ARN
value: "arn:aws:iam::111122223333:role/role-2"
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"
volumeMounts:
- mountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount/"
name: aws-token-2
volumes:
- name: aws-token-1
projected:
sources:
- serviceAccountToken:
audience: "sts.amazonaws.com"
expirationSeconds: 86400
path: token
- name: aws-token-2
projected:
sources:
- serviceAccountToken:
audience: "some-other-client-id-arbitrary-string"
expirationSeconds: 86400
path: token
```

Now for each role, you'll need to add a key to the `StringEquals` condition map
in the role's trust policy specifying the proper audience (`aud`) for each role.

`role-1` trust policy:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:my-serviceaccount",
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:aud": "sts.amazonaws.com"
}
}
}
]
}
```

And `role-2` trust policy:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:my-serviceaccount",
"oidc.us-west-2.eks.amazonaws.com/CLUSTER_ID:aud": "some-other-client-id-arbitrary-string"
}
}
}
]
}
```

## Code of Conduct
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)

Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ require (
github.com/json-iterator/go v1.1.6 // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/pkg/errors v0.8.0
github.com/prometheus/client_golang v0.9.3
github.com/spf13/pflag v1.0.3
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.5.0
k8s.io/api v0.0.0-20190606204050-af9c91bd2759
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
k8s.io/client-go v11.0.1-0.20190606204521-b8faab9c5193+incompatible
k8s.io/klog v0.3.0
k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208 // indirect
k8s.io/kubernetes v1.14.3
k8s.io/utils v0.0.0-20190529001817-6999998975a7 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
sigs.k8s.io/yaml v1.1.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/api v0.0.0-20190606204050-af9c91bd2759 h1:T8xTLSBgKsq1bkiAwG9xamEydWVpBv9fHl5S/TDh3OU=
Expand Down
17 changes: 14 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func main() {
mountPath := flag.String("token-mount-path", "/var/run/secrets/eks.amazonaws.com/serviceaccount", "The path to mount tokens")
tokenExpiration := flag.Int64("token-expiration", 86400, "The token expiration")
region := flag.String("aws-default-region", "", "If set, AWS_DEFAULT_REGION and AWS_REGION will be set to this value in mutated containers")
regionalSTS := flag.String("sts-regional-endpoint", "", `Value to inject for the AWS_STS_REGIONAL_ENDPOINTS env var in mutated pods. Defaults to no value/injection. Must be "regional" or ""`)

version := flag.Bool("version", false, "Display the version and exit")

Expand All @@ -80,6 +81,16 @@ func main() {
os.Exit(0)
}

{
validSTSValues := map[string]bool{
"": false,
"regional": false,
}
if _, ok := validSTSValues[*regionalSTS]; !ok {
klog.Fatalf(`Invalid value "%s" for sts-regional-endpoint. Must be "", "regional"`, *regionalSTS)
}
}

config, err := clientcmd.BuildConfigFromFlags(*apiURL, *kubeconfig)
if err != nil {
klog.Fatalf("Error creating config: %v", err.Error())
Expand All @@ -105,6 +116,7 @@ func main() {
handler.WithMountPath(*mountPath),
handler.WithServiceAccountCache(saCache),
handler.WithRegion(*region),
handler.WithRegionalSTS(*regionalSTS),
)

addr := fmt.Sprintf(":%d", *port)
Expand All @@ -124,7 +136,6 @@ func main() {
fmt.Fprintf(w, "ok")
})


tlsConfig := &tls.Config{}

if *inCluster {
Expand Down Expand Up @@ -180,8 +191,8 @@ func main() {
handler.ShutdownOnTerm(server, time.Duration(10)*time.Second)

metricsServer := &http.Server{
Addr: metricsAddr,
Handler: metricsMux,
Addr: metricsAddr,
Handler: metricsMux,
}

go func() {
Expand Down
25 changes: 17 additions & 8 deletions pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import (
)

type CacheResponse struct {
RoleARN string
Audience string
RoleARN string
Audience string
RegionalSTS string
}

type ServiceAccountCache interface {
Start()
Get(name, namespace string) (role, aud string)
Get(name, namespace string) (role, aud, regionalSTS string)
}

type serviceAccountCache struct {
Expand All @@ -47,13 +48,13 @@ type serviceAccountCache struct {
defaultAudience string
}

func (c *serviceAccountCache) Get(name, namespace string) (role, aud string) {
func (c *serviceAccountCache) Get(name, namespace string) (role, aud, regionalSTS string) {
klog.V(5).Infof("Fetching sa %s/%s from cache", namespace, name)
resp := c.get(name, namespace)
if resp == nil {
return "", ""
return "", "", ""
}
return resp.RoleARN, resp.Audience
return resp.RoleARN, resp.Audience, resp.RegionalSTS
}

func (c *serviceAccountCache) get(name, namespace string) *CacheResponse {
Expand All @@ -78,10 +79,18 @@ func (c *serviceAccountCache) addSA(sa *v1.ServiceAccount) {
resp := &CacheResponse{}
if ok {
resp.RoleARN = arn
resp.Audience = c.defaultAudience
if audience, ok := sa.Annotations[c.annotationPrefix+"/audience"]; ok {
resp.Audience = audience
} else {
resp.Audience = c.defaultAudience
}
resp.RegionalSTS = ""
regionalVal, ok := sa.Annotations[c.annotationPrefix+"/sts-regional-endpoints"]
if ok {
if regionalVal == "regional" || regionalVal == "legacy" || regionalVal == "" {
resp.RegionalSTS = regionalVal
} else {
klog.V(4).Infof("Ignoring service account %s/%s invalid STS regional endpoint annotation value: %s", sa.Namespace, sa.Name, regionalVal)
}
}
}
klog.V(5).Infof("Adding sa %s/%s to cache", sa.Name, sa.Namespace)
Expand Down
17 changes: 11 additions & 6 deletions pkg/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,33 @@ func TestSaCache(t *testing.T) {
testSA.Name = "default"
testSA.Namespace = "default"
roleArn := "arn:aws:iam::111122223333:role/s3-reader"
testSA.Annotations = map[string]string{"eks.amazonaws.com/role-arn": roleArn}
testSA.Annotations = map[string]string{
"eks.amazonaws.com/role-arn": roleArn,
"eks.amazonaws.com/sts-regional-endpoints": "regional",
}

cache := &serviceAccountCache{
cache: map[string]*CacheResponse{},
defaultAudience: "sts.amazonaws.com",
annotationPrefix: "eks.amazonaws.com",
}

role, aud := cache.Get("default", "default")
role, aud, regionalSTS := cache.Get("default", "default")

if role != "" || aud != "" {
t.Errorf("Expected role and aud to be empty, got %s, %s", role, aud)
if role != "" || aud != "" || regionalSTS != "" {
t.Errorf("Expected role and aud to be empty, got %s, %s, %s", role, aud, regionalSTS)
}

cache.addSA(testSA)

role, aud = cache.Get("default", "default")
role, aud, regionalSTS = cache.Get("default", "default")
if role != roleArn {
t.Errorf("Expected role to be %s, got %s", roleArn, role)
}
if aud != "sts.amazonaws.com" {
t.Errorf("Expected aud to be sts.amzonaws.com, got %s", aud)
}

if regionalSTS != "regional" {
t.Errorf("Expected regional STS to be `regional`, got %s", regionalSTS)
}
}
Loading

0 comments on commit 454d3b4

Please sign in to comment.