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

feat(eks): IAM roles for service accounts #6062

Merged
merged 28 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
48d2c19
feat(eks): least privilege service accounts
vlesierse Feb 2, 2020
48d7262
Create service account and IAM Role
vlesierse Feb 10, 2020
19f5724
feat(eks): least privilege service accounts
vlesierse Feb 2, 2020
54ed81b
Create service account and IAM Role
vlesierse Feb 10, 2020
dfb1dc6
Try Lazy.stringValue
vlesierse Mar 7, 2020
96a142f
Merge remote-tracking branch 'vlesierse/eks/opidc-sa' into eks/opidc-sa
vlesierse Mar 7, 2020
f4059d7
Prepare for OIdC role custom resource
vlesierse Mar 12, 2020
db0abca
Merge branch 'master' into eks/opidc-sa
vlesierse Mar 12, 2020
2c97cca
Working support for IAM linked service accounts
vlesierse Mar 13, 2020
0f4efa9
Merge branch 'master' into eks/opidc-sa
vlesierse Apr 16, 2020
ab9b16f
Merge branch 'master' into eks/opidc-sa
vlesierse May 8, 2020
a49fad6
Using OpenIdConnectProvider constuct
vlesierse May 9, 2020
b3611ab
Refactoring to cluster.openIdConnectProvider
vlesierse May 10, 2020
6748dbb
Add serviceAccount* properties
vlesierse May 10, 2020
ffd6b5a
Add EKS issuer and thumbprint
vlesierse May 11, 2020
9fd5f5b
Introduction to WebIdentityPrincipal
vlesierse May 11, 2020
5e8a29a
Add convenience addServiceAccount
vlesierse May 11, 2020
f835cb8
Add and use OpenIdConnectPrincipal
vlesierse May 16, 2020
724a2b8
Add additional tests
vlesierse May 16, 2020
e49893d
Merge branch 'master' into eks/opidc-sa
vlesierse May 17, 2020
0e78d6c
Add service account tests and changes after review
vlesierse May 17, 2020
1d3f8e0
Merge branch 'master' into eks/opidc-sa
vlesierse May 17, 2020
f90bf1f
Apply changes to eks-cluster integration test
vlesierse May 18, 2020
909b778
Move OpenIDConnectPrincipal in README
vlesierse May 18, 2020
34d029a
Merge branch 'master' into eks/opidc-sa
mergify[bot] May 18, 2020
1f303da
Merge branch 'master' into eks/opidc-sa
mergify[bot] May 18, 2020
f069c4e
Merge branch 'master' into eks/opidc-sa
mergify[bot] May 18, 2020
afa1075
Merge branch 'master' into eks/opidc-sa
mergify[bot] May 18, 2020
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
30 changes: 29 additions & 1 deletion packages/@aws-cdk/aws-eks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,9 +518,37 @@ cluster.addCapacity('BottlerocketNodes', {

To define only Bottlerocket capacity in your cluster, set `defaultCapacity` to `0` when you define the cluster as described above.

Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity.
Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity.

### Service Accounts

With services account you can provide Kubernetes Pods access to AWS resources.

```ts
// add service account
const serviceAccount = cluster.addServiceAccount('MyServiceAccount');

const bucket = new Bucket(this, 'Bucket');
bucket.grantReadWrite(serviceAccount);

cluster.addResource('mypod', {
apiVersion: 'v1',
kind: 'Pod',
metadata: { name: 'mypod' },
spec: {
containers: [
{
name: 'hello',
image: 'paulbouwer/hello-kubernetes:1.5',
ports: [ { containerPort: 8080 } ],
serviceAccountName: serviceAccount.serviceAccountName
}
]
}
});
```

> Warning: Currently there are no condition set on the IAM Role which results that there are no restrictions on other pods to assume the role. This will be improved in the near future.

### Roadmap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export class ClusterResourceHandler extends ResourceHandler {
Endpoint: cluster.endpoint,
Arn: cluster.arn,
CertificateAuthorityData: cluster.certificateAuthority?.data,
OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer,
},
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class ClusterResource extends Construct {
public readonly attrEndpoint: string;
public readonly attrArn: string;
public readonly attrCertificateAuthorityData: string;
public readonly attrOpenIdConnectIssuerUrl: string;
public readonly ref: string;

/**
Expand Down Expand Up @@ -124,6 +125,7 @@ export class ClusterResource extends Construct {
this.attrEndpoint = Token.asString(resource.getAtt('Endpoint'));
this.attrArn = Token.asString(resource.getAtt('Arn'));
this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData'));
this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl'));
}

/**
Expand Down
58 changes: 58 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { KubernetesPatch } from './k8s-patch';
import { KubernetesResource } from './k8s-resource';
import { KubectlProvider } from './kubectl-provider';
import { Nodegroup, NodegroupOptions } from './managed-nodegroup';
import { ServiceAccount, ServiceAccountOptions } from './service-account';
import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data';

// defaults are based on https://eksctl.io
Expand Down Expand Up @@ -342,6 +343,8 @@ export class Cluster extends Resource implements ICluster {
*/
private _awsAuth?: AwsAuth;

private _openIdConnectProvider?: iam.OpenIdConnectProvider;

private _spotInterruptHandler?: HelmChart;

private readonly version: string | undefined;
Expand Down Expand Up @@ -617,6 +620,48 @@ export class Cluster extends Resource implements ICluster {
return this._awsAuth;
}

/**
* If this cluster is kubectl-enabled, returns the OpenID Connect issuer url.
* This is because the values is only be retrieved by the API and not exposed
* by CloudFormation. If this cluster is not kubectl-enabled (i.e. uses the
* stock `CfnCluster`), this is `undefined`.
* @attribute
*/
public get clusterOpenIdConnectIssuerUrl(): string {
if (!this._clusterResource) {
throw new Error('unable to obtain OpenID Connect issuer URL. Cluster must be kubectl-enabled');
}

return this._clusterResource.attrOpenIdConnectIssuerUrl;
}

/**
* An `OpenIdConnectProvider` resource associated with this cluster, and which can be used
* to link this cluster to AWS IAM.
*
* A provider will only be defined if this property is accessed (lazy initialization).
*/
public get openIdConnectProvider() {
if (!this.kubectlEnabled) {
throw new Error('Cannot specify a OpenID Connect Provider if kubectl is disabled');
}

if (!this._openIdConnectProvider) {
this._openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'OpenIdConnectProvider', {
url: this.clusterOpenIdConnectIssuerUrl,
clientIds: [ 'sts.amazonaws.com' ],
/**
* For some reason EKS isn't validating the root certificate but a intermediat certificate
* which is one level up in the tree. Because of the a constant thumbprint value has to be
* stated with this OpenID Connect provider. The certificate thumbprint is the same for all the regions.
*/
thumbprints: [ '9e99a48a9960b14926bb7f3b02e22da2b0ab7280' ],
eladb marked this conversation as resolved.
Show resolved Hide resolved
});
}

return this._openIdConnectProvider;
}

/**
* Defines a Kubernetes resource in this cluster.
*
Expand Down Expand Up @@ -657,6 +702,19 @@ export class Cluster extends Resource implements ICluster {
});
}

/**
* Adds a service account to this cluster.
*
* @param id the id of this service account
* @param options service account options
*/
public addServiceAccount(id: string, options: ServiceAccountOptions) {
return new ServiceAccount(this, id, {
...options,
cluster: this,
});
}

/**
* Returns the role ARN for the cluster creation role for kubectl-enabled
* clusters.
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-eks/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from './helm-chart';
export * from './k8s-patch';
export * from './k8s-resource';
export * from './fargate-cluster';
export * from './service-account';
export * from './managed-nodegroup';
95 changes: 95 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/service-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { AddToPrincipalPolicyResult, IPrincipal, IRole, OpenIdConnectPrincipal, PolicyStatement, PrincipalPolicyFragment, Role } from '@aws-cdk/aws-iam';
import { Construct } from '@aws-cdk/core';
import { Cluster } from './cluster';

/**
* Options for `ServiceAccount`
*/
export interface ServiceAccountOptions {
/**
* The name of the service account.
* @default - If no name is given, it will use the id of the resource.
*/
readonly name?: string;

/**
* The namespace of the service account.
* @default "default"
*/
readonly namespace?: string;
}

/**
* Properties for defining service accounts
*/
export interface ServiceAccountProps extends ServiceAccountOptions {
/**
* The cluster to apply the patch to.
* [disable-awslint:ref-via-interface]
*/
readonly cluster: Cluster;
}

/**
* Service Account
*/
export class ServiceAccount extends Construct implements IPrincipal {

/**
* The role which is linked to the service account.
*/
public readonly role: IRole;

public readonly assumeRoleAction: string;
public readonly grantPrincipal: IPrincipal;
public readonly policyFragment: PrincipalPolicyFragment;

/**
* The name of the service account.
*/
public readonly serviceAccountName: string;

/**
* The namespace where the service account is located in.
*/
public readonly serviceAccountNamespace: string;

constructor(scope: Construct, id: string, props: ServiceAccountProps) {
super(scope, id);

const { cluster } = props;
this.serviceAccountName = props.name ?? this.node.uniqueId.toLowerCase();
this.serviceAccountNamespace = props.namespace ?? 'default';

this.role = new Role(this, 'Role', {
assumedBy: new OpenIdConnectPrincipal(cluster.openIdConnectProvider),
});

this.assumeRoleAction = this.role.assumeRoleAction;
this.grantPrincipal = this.role.grantPrincipal;
this.policyFragment = this.role.policyFragment;

cluster.addResource('ServiceAccount', {
apiVersion: 'v1',
kind: 'ServiceAccount',
metadata: {
name: this.serviceAccountName,
namespace: this.serviceAccountNamespace,
labels: {
'app.kubernetes.io/name': this.serviceAccountName,
},
annotations: {
'eks.amazonaws.com/role-arn': this.role.roleArn,
},
},
});
}

public addToPolicy(statement: PolicyStatement): boolean {
return this.role.addToPolicy(statement);
}

public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
return this.role.addToPrincipalPolicy(statement);
}
}
38 changes: 19 additions & 19 deletions packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -2224,7 +2224,7 @@
},
"/",
{
"Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2"
"Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16"
},
"/",
{
Expand All @@ -2234,7 +2234,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8"
"Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B"
}
]
}
Expand All @@ -2247,7 +2247,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8"
"Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B"
}
]
}
Expand All @@ -2257,11 +2257,11 @@
]
},
"Parameters": {
"referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket278A73D2Ref": {
"Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34"
"referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3Bucket21CE03E4Ref": {
"Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2"
},
"referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKeyD7A198A8Ref": {
"Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E"
"referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKey7161DBC6Ref": {
"Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006"
},
"referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": {
"Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C"
Expand Down Expand Up @@ -2406,17 +2406,17 @@
}
},
"Parameters": {
"AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34": {
"AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2": {
"Type": "String",
"Description": "S3 bucket for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\""
"Description": "S3 bucket for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\""
},
"AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E": {
"AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006": {
"Type": "String",
"Description": "S3 key for asset version \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\""
"Description": "S3 key for asset version \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\""
},
"AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5ArtifactHash2145581C": {
"AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeArtifactHash058BD37E": {
"Type": "String",
"Description": "Artifact hash for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\""
"Description": "Artifact hash for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\""
},
"AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": {
"Type": "String",
Expand All @@ -2442,17 +2442,17 @@
"Type": "String",
"Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\""
},
"AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2": {
"AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16": {
"Type": "String",
"Description": "S3 bucket for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\""
"Description": "S3 bucket for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\""
},
"AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8": {
"AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B": {
"Type": "String",
"Description": "S3 key for asset version \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\""
"Description": "S3 key for asset version \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\""
},
"AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2ArtifactHashAF96C5C2": {
"AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9ArtifactHash93C28EE4": {
"Type": "String",
"Description": "Artifact hash for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\""
"Description": "Artifact hash for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\""
},
"AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": {
"Type": "String",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export = {
Endpoint: 'http://endpoint',
Arn: 'arn:cluster-arn',
CertificateAuthorityData: 'certificateAuthority-data',
OpenIdConnectIssuerUrl: undefined,
},
});
test.done();
Expand Down Expand Up @@ -420,6 +421,7 @@ export = {
Endpoint: 'http://endpoint',
Arn: 'arn:cluster-arn',
CertificateAuthorityData: 'certificateAuthority-data',
OpenIdConnectIssuerUrl: undefined,
},
});
test.done();
Expand Down
Loading