From 7c3099e958d7bf0ddb5a7b08afb672a0c652b27d Mon Sep 17 00:00:00 2001 From: Adam Brodziak Date: Mon, 7 Mar 2022 15:00:45 +0100 Subject: [PATCH] feat(eks): Service Account names validation (#19251) Kubernetes has got specific requirements to names of resources: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ Without checks user will learn about invalid name at `cdk deploy` stage. That could leave EKS cluster in an inconsistent state. fixes #18189 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-eks/lib/service-account.ts | 34 +++++++ .../aws-eks/test/service-account.test.ts | 97 +++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts index c49e2a944a765..a330aa41e0df5 100644 --- a/packages/@aws-cdk/aws-eks/lib/service-account.ts +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -14,12 +14,18 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; export interface ServiceAccountOptions { /** * The name of the service account. + * + * The name of a ServiceAccount object must be a valid DNS subdomain name. + * https://kubernetes.io/docs/tasks/configure-pod-container/configure-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. + * + * All namespace names must be valid RFC 1123 DNS labels. + * https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#namespaces-and-dns * @default "default" */ readonly namespace?: string; @@ -65,6 +71,16 @@ export class ServiceAccount extends CoreConstruct implements IPrincipal { this.serviceAccountName = props.name ?? Names.uniqueId(this).toLowerCase(); this.serviceAccountNamespace = props.namespace ?? 'default'; + // From K8s docs: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ + if (!this.isValidDnsSubdomainName(this.serviceAccountName)) { + throw RangeError('The name of a ServiceAccount object must be a valid DNS subdomain name.'); + } + + // From K8s docs: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#namespaces-and-dns + if (!this.isValidDnsLabelName(this.serviceAccountNamespace)) { + throw RangeError('All namespace names must be valid RFC 1123 DNS labels.'); + } + /* Add conditions to the role to improve security. This prevents other pods in the same namespace to assume the role. * See documentation: https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html */ @@ -117,4 +133,22 @@ export class ServiceAccount extends CoreConstruct implements IPrincipal { public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { return this.role.addToPrincipalPolicy(statement); } + + /** + * If the value is a DNS subdomain name as defined in RFC 1123, from K8s docs. + * + * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + */ + private isValidDnsSubdomainName(value: string): boolean { + return value.length <= 253 && /^[a-z0-9]+[a-z0-9-.]*[a-z0-9]+$/.test(value); + } + + /** + * If the value follows DNS label standard as defined in RFC 1123, from K8s docs. + * + * https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names + */ + private isValidDnsLabelName(value: string): boolean { + return value.length <= 63 && /^[a-z0-9]+[a-z0-9-]*[a-z0-9]+$/.test(value); + } } diff --git a/packages/@aws-cdk/aws-eks/test/service-account.test.ts b/packages/@aws-cdk/aws-eks/test/service-account.test.ts index e4db2f6680a97..7ece468d200d0 100644 --- a/packages/@aws-cdk/aws-eks/test/service-account.test.ts +++ b/packages/@aws-cdk/aws-eks/test/service-account.test.ts @@ -174,4 +174,101 @@ describe('service account', () => { }); }); + + describe('Service Account name must follow Kubernetes spec', () => { + test('throw error on capital letters', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + name: 'XXX', + })) + // THEN + .toThrowError(RangeError); + }); + test('throw error if ends with dot', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + name: 'test.', + })) + // THEN + .toThrowError(RangeError); + }); + test('dot in the name is allowed', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + const valueWithDot = 'test.name'; + + // WHEN + const sa = cluster.addServiceAccount('InvalidServiceAccount', { + name: valueWithDot, + }); + + // THEN + expect(sa.serviceAccountName).toEqual(valueWithDot); + }); + test('throw error if name is too long', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + name: 'x'.repeat(255), + })) + // THEN + .toThrowError(RangeError); + }); + }); + + describe('Service Account namespace must follow Kubernetes spec', () => { + test('throw error on capital letters', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + namespace: 'XXX', + })) + // THEN + .toThrowError(RangeError); + }); + test('throw error if ends with dot', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + namespace: 'test.', + })) + // THEN + .toThrowError(RangeError); + }); + test('throw error if dot is in the name', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + const valueWithDot = 'test.name'; + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + namespace: valueWithDot, + })) + // THEN + .toThrowError(RangeError); + }); + test('throw error if name is too long', () => { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + expect(() => cluster.addServiceAccount('InvalidServiceAccount', { + namespace: 'x'.repeat(65), + })) + // THEN + .toThrowError(RangeError); + }); + }); });