Kubernetes Identity

AWS exposes an Identity Access and Management (IAM) API which can be used to grant permissions to both human and bot users. Using this API, IAM User accounts can be slotted into IAM Groups (e.g., the networkAdmins IAM Group), which can then be allocated baseline permissions using IAM Policies.

AWS workloads (e.g., AWS Lambdas) can also be granted permissions temporarily, without the need for usernames and passwords, using IAM Roles.

In Crosswalk for AWS we showcase how to define IAM:

The full code for this stack is on GitHub.

Azure exposes an Active Directory API which can be used to grant permissions to both human and bot users. Using this API, IAM User accounts can be slotted into IAM Groups (e.g., the networkAdmins IAM Group), which can then be allocated baseline permissions using IAM Permissions.

Azure services can also be granted permissions temporarily, without the need for usernames and passwords, using Applications and ServicePrincipals.

The full code for this stack is on GitHub.

GCP exposes an Identity Access and Management (IAM) API which can be used to grant permissions to both human and bot users. Using this API, IAM Members can be slotted into end users, IAM Groups (e.g., the networkAdmins IAM Group), and GSuite accounts, and can then be allocated IAM Roles with baseline permissions using IAM Policies.

GCP services can also be granted permissions temporarily, without the need for usernames and passwords, using ServiceAccounts.

The full code for this stack is on GitHub.

Create an IAM Role for Admins

Create an admin role in AWS that assumes the AWS account caller, and attach EKS admin policies to the role. This role will be mapped into the system:masters group in Kubernetes RBAC.

import * as aws from "@pulumi/aws";

// Create the EKS cluster admins role.
const adminsName = "admins";
const adminsIamRole = new aws.iam.Role(`${adminsName}-eksClusterAdmin`, {
    assumeRolePolicy: aws.getCallerIdentity().then(id =>
        aws.iam.assumeRolePolicyForPrincipal({"AWS": `arn:aws:iam::${id.accountId}:root`}))
})
const adminsIamRolePolicy = new aws.iam.RolePolicy(`${adminsName}-eksClusterAdminPolicy`, {
    role: adminsIamRole,
    policy: {
        Version: "2012-10-17",
        Statement: [
            { Effect: "Allow", Action: ["eks:*", "ec2:DescribeImages"], Resource: "*", },
            { Effect: "Allow", Action: "iam:PassRole", Resource: "*"},
        ],
    },
},
    { parent: adminsIamRole },
);

Authenticate as the admin role.

$ aws sts assume-role --role-arn `pulumi stack output adminsIamRoleArn` --role-session-name k8s-admin

Azure AD integration with AKS requires that two Azure AD application objects be created: a server component to provide authentication, and a client component used by the CLI for authentication that works with the server component. We will create both in the sections below.

Note: The Server and Client Application registrations will be created with the desired accesses, but an administrator will need to grant the Server Application consent before they can be used in proceeding stacks.

See the official docs for more details.

Prerequisites

To facilitate working with Azure, it is recommended that you create a new ServicePrincipal with the proper permissions needed to create all of the resources in each stack.

$ az ad sp create-for-rbac --name MyServicePrincipal
$ az login --service-principal --username $ARM_CLIENT_ID --password $ARM_CLIENT_SECRET --tenant $ARM_TENANT_ID

Ensure the ServicePrincipal has the following permissions for the legacy, and current Graph APIs:

Azure Active Directory Graph (Legacy)

Permission NameType
Application.ReadWrite.AllApplication
Directory.ReadWrite.AllApplication
Group.ReadWrite.AllDelegated
User.Read.AllDelegated

Microsoft Graph

Permission NameType
Application.ReadWrite.AllApplication
Directory.ReadWrite.AllApplication
Group.ReadWrite.AllDelegated
User.ReadDelegated

Create an IAM Server Application and ServicePrincipal

Create an Azure server Application and ServicePrincipal, and attach it AD permissions.

Note: The Application registration will be created with the desired accesses, but an administrator will need to grant consent before they can be used in proceeding stacks.

import * as azuread from "@pulumi/azuread";

// Create the server application in Azure AD.
const applicationServer = new azuread.Application(`${name}-app-server`, {
    replyUrls: ["http://k8s_server"],
    type: "webapp/api",
    groupMembershipClaims: "All",
    requiredResourceAccesses: [
        // Windows Azure Active Directory API
        {
            resourceAppId: "00000002-0000-0000-c000-000000000000",
            resourceAccesses: [{
                // DELEGATED PERMISSIONS: "Sign in and read user profile"
                id: "311a71cc-e848-46a1-bdf8-97ff7156d8e6",
                type: "Scope",
            }],
        },
        // MicrosoftGraph API
        {
            resourceAppId: "00000003-0000-0000-c000-000000000000",
            resourceAccesses: [
                // APPLICATION PERMISSIONS: "Read directory data"
                {
                    id: "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
                    type: "Role",
                },
                // DELEGATED PERMISSIONS: "Sign in and read user profile"
                {
                    id: "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
                    type: "Scope",
                },
                // DELEGATED PERMISSIONS: "Read directory data"
                {
                    id: "06da0dbc-49e2-44d2-8312-53f166ab848a",
                    type: "Scope",
                },
            ],
        },
    ],
});

// Create the server application Service Principal.
const principalServer = new azuread.ServicePrincipal(`${name}-sp-server`, {
    applicationId: applicationServer.applicationId,
});

// Export outputs.
export const adServerAppId = applicationServer.applicationId;

Create an IAM Client Application and ServicePrincipal

Create an Azure client Application and ServicePrincipal, and attach it AD permissions.

import * as azuread from "@pulumi/azuread";

// Create the client application in Azure AD.
const applicationClient = new azuread.Application(`${name}-app-client`, {
    replyUrls: ["http://k8s_server"],
    type: "native",
    requiredResourceAccesses: [
        // Windows Azure Active Directory API
        {
            resourceAppId: "00000002-0000-0000-c000-000000000000",
            resourceAccesses: [{
                // DELEGATED PERMISSIONS: "Sign in and read user profile"
                id: "311a71cc-e848-46a1-bdf8-97ff7156d8e6",
                type: "Scope",
            }],
        },
        // AKS ad application server
        {
            resourceAppId: applicationServer.applicationId,
            resourceAccesses: [{
                // Server app Oauth2 permissions id
                id: applicationServer.oauth2Permissions[0].id,
                type: "Scope",
            }],
        },
    ],
});

// Create the client application Service Principal.
const principalClient = new azuread.ServicePrincipal(`${name}-sp-client`, {
    applicationId: applicationClient.applicationId,
});

// Export outputs.
export const adClientAppId = applicationClient.applicationId;

See the official docs for more details.

Prerequisites

To facilitate working with GCP, it is recommended that you create a new ServiceAccount with the proper permissions needed to create all of the resources in each stack. We’ll set this ServiceAccount to be bound to GCP roles the with permissions of role/editor and roles/container.clusterAdmin.

Create the ServiceAccount and its secret key to use for auth. Then bind it to the roles/editor role, and authenticate as the ServiceAccount to use with the stacks.

$ gcloud iam service-accounts create my-service-account --description MyServiceAccount --display-name MyServiceAccount
$ gcloud iam service-accounts keys create ~/key.json --iam-account my-service-account@pulumi-development.iam.gserviceaccount.com
$ gcloud projects add-iam-policy-binding pulumi-development --member serviceAccount:my-service-account@pulumi-development.iam.gserviceaccount.com --role roles/editor
$ gcloud projects add-iam-policy-binding pulumi-development --member serviceAccount:my-service-account@pulumi-development.iam.gserviceaccount.com --role roles/container.clusterAdmin
$ gcloud auth activate-service-account --key-file ~/key.json

Create an IAM Role and ServiceAccount for Admins

Create an admin role in GCP and attach admin policies to the role. This role will be bound to the admin service account.

import * as gcp from "@pulumi/gcp";

// Create the GKE cluster admins ServiceAccount.
const adminsName = "admins";
const adminsIamServiceAccount = new gcp.serviceAccount.Account(adminsName, {
    project: config.project,
    accountId: `k8s-${adminsName}`,
    displayName: "Kubernetes Admins",
});

// Bind the admin ServiceAccount to be a GKE cluster admin.
util.bindToRole(`${adminsName}-k8s`, adminsIamServiceAccount, {
    project: config.project,
    roles: ["roles/container.clusterAdmin", "roles/container.developer"],
});

// Bind the admin ServiceAccount to be a CloudSQL admin.
util.bindToRole(`${adminsName}-cloudsql`, adminsIamServiceAccount, {
    project: config.project,
    roles: ["roles/cloudsql.admin"],
});

// Export the admins ServiceAccount key.
const adminsIamServiceAccountKey = util.createServiceAccountKey(`${adminsName}Key`, adminsIamServiceAccount);

// Export the admins ServiceAccount client secret to authenticate as this service account.
export const adminsIamServiceAccountSecret = util.clientSecret(adminsIamServiceAccountKey);

// Helper to bind the service account to a given role.
export function bindToRole(
    name: string,
    sa: gcp.serviceAccount.Account,
    args: { project: pulumi.Input<string>; roles: string[]})
{
    args.roles.forEach((role, index) => {
        new gcp.projects.IAMBinding(`${name}-${index}`, {
            project: args.project,
            role: role,
            members: [sa.email.apply(email => `serviceAccount:${email}`)],
        });
    })
}

// Helper to create new service account key.
export function createServiceAccountKey(name: string, sa: gcp.serviceAccount.Account): gcp.serviceAccount.Key {
    return new gcp.serviceAccount.Key(name, { serviceAccountId: sa.name });
}

// Helper to export service account for authentication use.
export function clientSecret(key: gcp.serviceAccount.Key): pulumi.Output<any> {
    return key.privateKey.apply(key => JSON.parse(Buffer.from(key, "base64").toString("ascii")));
}

Authenticate as the admin ServiceAccount by exporting the key, and signing into gcloud with it.

$ pulumi stack output adminsIamServiceAccountSecret > k8s-admin-sa-key.json
$ gcloud auth activate-service-account --key-file k8s-admin-sa-key.json

Create an IAM Role for Managing CloudSQL Databases

import * as gcp from "@pulumi/gcp";

// Bind the admin ServiceAccount to be a CloudSQL admin.
util.bindToRole(`${adminsName}CloudSqlAdmin`, adminsIamServiceAccount, {
    project: config.project,
    roles: ["roles/cloudsql.admin"],
});

Create an IAM Role for Developers

Create a developer role in AWS that assumes the AWS account caller. This role will be mapped into a limited developer role in Kubernetes RBAC.

import * as aws from "@pulumi/aws";

// Create the EKS cluster developers role.
const devName = "devs";
const devsIamRole = new aws.iam.Role(`${devName}-eksClusterDeveloper`, {
    assumeRolePolicy: aws.getCallerIdentity().then(id =>
        aws.iam.assumeRolePolicyForPrincipal({"AWS": `arn:aws:iam::${id.accountId}:root`}))
})

Authenticate as the devs role.

$ aws sts assume-role --role-arn `pulumi stack output devsIamRoleArn` --role-session-name k8s-devs

Create an IAM Group for Admins

Create an admins group in Azure. This group will be mapped into the system:mastersgroup in Kubernetes RBAC.

import * as azure from "@pulumi/azure";
import * as azuread from "@pulumi/azuread";

const clientConfig = azure.core.getClientConfig();
const currentPrincipal = clientConfig.objectId;

const admins = new azuread.Group("admins", {
    name: "pulumi:admins",
    members: [
        currentPrincipal,
    ],
});

Create an IAM Group for Developers

Create a developer group in Azure. This group will be mapped into a limited developer role in Kubernetes RBAC.

Add a new user in AD, or get an existing user to add to the new AD group.

Note: On deletion of this Pulumi stack you cannot use the ServicePrincipal to delete the Group, as ActiveDirectory does not allow Application registrations to delete AD Groups. Instead, you must authenticate as a user, and ensure that the user has admin rights in AD to be able to delete the group.

import * as azuread from "@pulumi/azuread";

/* Create a new user in AD.
const dev = new azuread.User("k8s-dev", {
    userPrincipalName: "k8sdev@example.com",
    displayName: "Kubernetes Dev",
    password: "Qjker21!G",
});
*/

// Get an existing AD user.
const dev = azuread.getUser({
    userPrincipalName: "alice@example.com",
});

// Create the AD group for Developers.
const devs = new azuread.Group("devs", {
    name: "pulumi:devs",
    members: [
        // Assign a new or existing user to the group.
        dev.objectId,
    ],
});

// Export outputs.
export const adGroupDevs = devs.name;

Create an IAM Role and ServiceAccount for Developers

Create a developer role in GCP that will be mapped into a limited developer role in Kubernetes RBAC.

import * as gcp from "@pulumi/gcp";

// Create the GKE cluster developers ServiceAccount.
const devsName = "devs";
const devsIamServiceAccount = new gcp.serviceAccount.Account(devsName, {
    project: config.project,
    accountId: `k8s-${devsName}`,
    displayName: "Kubernetes Developers",
});

// Bind the devs ServiceAccount to be a GKE cluster developer.
util.bindToRole(`${devsName}-k8s`, devsIamServiceAccount, {
    project: config.project,
    roles: ["roles/container.developer"],
});

// Export the devs ServiceAccount key.
const devsIamServiceAccountKey = util.createServiceAccountKey(`${devsName}Key`, devsIamServiceAccount);

// Export the devs ServiceAccount client secret to authenticate as this service account.
export const devsIamServiceAccountClientSecret = util.clientSecret(devsIamServiceAccountKey);

Authenticate as the dev ServiceAccount by exporting the key, and signing into gcloud with it.

$ pulumi stack output devsIamServiceAccountSecret > k8s-devs-sa-key.json
$ gcloud auth activate-service-account --key-file k8s-devs-sa-key.json

Create IAM Roles for EKS Node Groups

Create a node group worker role in AWS that assumes the EC2 Service, and attach required EKS cluster polices to the role. This role will be used by the node group in an instance profile during cluster configuration and creation.

// The managed policies EKS requires of nodegroups join a cluster.
const nodegroupManagedPolicyArns: string[] = [
    "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
    "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
];

// Create the standard node group worker role and attach the required policies.
const stdName = "standardNodeGroup";
const stdNodegroupIamRole = new aws.iam.Role(`${stdName}-eksClusterWorkerNode`, {
    assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({"Service": "ec2.amazonaws.com"})
})
attachPoliciesToRole(stdName, stdNodegroupIamRole, nodegroupManagedPolicyArns);

// Create the performant node group worker role and attach the required policies.
const perfName = "performanceNodeGroup";
const perfNodegroupIamRole = new aws.iam.Role(`${perfName}-eksClusterWorkerNode`, {
    assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({"Service": "ec2.amazonaws.com"})
})
attachPoliciesToRole(perfName, perfNodegroupIamRole, nodegroupManagedPolicyArns);

// Attach policies to a role.
function attachPoliciesToRole(name: string, role: aws.iam.Role, policyArns: string[]) {
    for (const policyArn of policyArns) {
        new aws.iam.RolePolicyAttachment(`${name}-${policyArn.split('/')[1]}`,
            { policyArn: policyArn, role: role },
        );
    }
}