Simplify Kubernetes RBAC in Amazon EKS with open source Pulumi packages
Posted on
One of the most common areas Kubernetes operators struggle with in production involves creating and managing role-based access control (RBAC). This is so daunting that RBAC is often not implemented, or implemented halfway, or the configuration becomes impossible to maintain.
Fortunately, Pulumi makes RBAC on Kuberenetes so easy that you’ll never create an insecure cluster again. In this post, we will contrast the traditional way of working with RBAC on EKS with using Pulumi.
Here are a few highlights:
- NO MORE YAMLs! Configuring YAMLs, operators or custom resources is now a thing in the past! You use TypeScript or JavaScript to program directly with our cloud SDK and connect all cloud services to your Kubernetes services with a simple reference to the object in your program.
- INCREASED DEVELOPMENT VELOCITY: You intuitively program Kubernetes objects with our SDK abstractions using minimal amount of code within hours instead of months. You “autocomplete” AWS, EKS, Kubernetes specifications within your IDE without understanding the entire API.
- EASY UPDATES: Changing a
roleRef
in aRoleBinding
, on one or multiple clusters involves updating your TypeScript fileindex.ts
and runningpulumi up
. The Pulumi Service allows you to share your stack with your team in your GitHub, GitLab, or Atlassian-based organization. - WORKFLOW AUTOMATION FOR RBAC AT SCALE: You can delete or update
multiple
RoleBindings or Roles
from your Pulumi stack source code. As you commit these changes to your repository, you can plan automated triggers that validate such changes as part of your CI/CD flow, whether you use Travis, CircleCI, AzureDevOps and more. Pulumi even has a GitHub Application for surfacing results within pull requests.
Prerequisites to work with Pulumi
Install pulumi
CLI and
set up your
AWS credentials.
Initialize a new
Pulumi project from available
templates. We use aws-typescript template
here to install all
dependencies and save the configuration.
$ brew install pulumi/tap/pulumi # download pulumi CLI
$ mkdir eks-rbac && cd eks-rbac
$ pulumi new aws-typescript
$ ls -l
-rw-r--r-- 1 nishidavidson staff 32 Apr 18 14:49 Pulumi.dev.yaml
-rw------- 1 nishidavidson staff 84 Apr 18 14:48 Pulumi.yaml
-rw------- 1 nishidavidson staff 273 Apr 18 14:48 index.ts
drwxr-xr-x 92 nishidavidson staff 2944 Apr 18 14:49 node_modules
-rw-r--r-- 1 nishidavidson staff 48352 Apr 18 14:49 package-lock.json
-rw------- 1 nishidavidson staff 228 Apr 18 14:48 package.json
-rw------- 1 nishidavidson staff 522 Apr 18 14:48 tsconfig.json
With Pulumi, you will modify and update the default index.ts
file with
AWS and EKS resource variable declarations. We show you how to add this
code as we contrast Pulumi’s approach with the sequential traditional
approach in the steps below. In the end, you will do a one-time run of
pulumi up
and watch all the steps in the Traditional Way come alive
simultaneously.
Step 1: Create three IAM Roles with a Trust-policy to map to Amazon EKS RBAC.
The Traditional-approach:
You sequentially create three IAM roles (clusterAdminRole
;
AutomationRole
; EnvProdRole
) with aws command line tool as shown
below:
$ aws iam create-role --role-name clusterAdminRole --assume-role-policy-document file://Role-Trust-Policy.json
{
"Role": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"AWS": "arn:aws:iam::xxxxxxxxxxxx:root"
},
"Effect": "Allow",
"Sid": ""
}
]
},
"RoleId": "AROASHIVKXX3SFFMUUEU6",
"CreateDate": "2019-04-17T17:43:03Z",
"RoleName": "clusterAdminRole",
"Path": "/",
"Arn": "arn:aws:iam::xxxxxxxxxxxx:role/clusterAdminRole"
}
}
$ cat Role-Trust-Policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "*"
}
]
}
The Pulumi-approach:
You update the default index.ts
file in your source code editor such
as VSCode as follows:
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";
import * as k8s from "@pulumi/kubernetes";
/*
* 1) Single step deployment of three IAM Roles
*/
function createIAMRole(name: string): aws.iam.Role {
// Create an IAM Role...
return new aws.iam.Role(`${name}`, {
assumeRolePolicy: `{
"Version": "2012-10-17",
"Statement":[
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::153052954103:root"
},
"Action": "sts:AssumeRole"
}
]
}
`,
tags: {
"clusterAccess": `${name}-usr`,
},
});
};
}
// Administrator AWS IAM clusterAdminRole with full access to all AWS resources
const clusterAdminRole = createIAMRole("clusterAdminRole");
// Administer Automation role for use in pipelines, e.g. gitlab CI, Teamcity, etc.
const AutomationRole = createIAMRole("AutomationRole");
// Administer Prod role for use in Prod environment
const EnvProdRole = createIAMRole("EnvProdRole");
Step 2: Create one EKS cluster. Validate cluster creation. Add the namespaces you need.
The Traditional-approach:
You go through the steps below to validate the cluster and k8s resource deployment based on your tool chain and your understanding of Kubernetes:
$ eksctl create cluster eks-nd-test
$ kubectl get no
NAME STATUS ROLES AGE VERSION
ip-192-168-41-125.us-east-2.compute.internal Ready <none> 11h v1.11.5
ip-192-168-5-250.us-east-2.compute.internal Ready <none> 11h v1.11.5
$ kubectl create -f automation-ns.yaml && kubectl create -f prod-ns.yaml
$ cat automation-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: automation
labels:
name: automation
The Pulumi-approach:
You use our API docs and your source-code editor to autocomplete the
default index.ts
file.
/*
* 2) Single step deployment of EKS cluster with the most important variables and a Simple Function to create namespaces
* automation and prod
*/
const vpc = new awsx.ec2.Vpc("vpc", {});
const cluster = new eks.Cluster("eks-cluster", {
vpcId : vpc.id,
subnetIds : vpc.publicSubnetIds,
instanceType : "t2.medium",
nodeRootVolumeSize: 200,
desiredCapacity : 1,
maxSize : 2,
minSize : 1,
deployDashboard : false,
vpcCniOptions : {
warmIpTarget : 4,
},
roleMappings : [
// Provides full administrator cluster access to the k8s cluster
{
groups : ["system:masters"],
roleArn : clusterAdminRole.arn,
username : "pulumi:admin-usr",
},
// Map IAM role arn "AutomationRoleArn" to the k8s user with name "automation-usr", e.g. gitlab CI
{
groups : ["pulumi:automation-grp"],
roleArn : AutomationRole.arn,
username : "pulumi:automation-usr",
},
// Map IAM role arn "EnvProdRoleArn" to the k8s user with name "prod-usr"
{
groups : ["pulumi:prod-grp"],
roleArn : EnvProdRole.arn,
username : "pulumi:prod-usr",
},
],
});
export const clusterName = cluster.eksCluster.name;
function createNewNamespace(name: string): k8s.core.v1.Namespace {
// Create new namespace
return new k8s.core.v1.Namespace(name, { metadata: { name: name } }, { provider: cluster.provider });
}
// Declare namespaces automation and prod.
const automation = createNewNamespace("automation");
const prod = createNewNamespace("prod");
Step 3: Understand Kubernetes RBAC. Declare the Kubernetes objects on the EKS cluster.
The Kubernetes RBAC API declares four top-level types that can be defined as YAMLs syntaxes: a) Role - represents a set of additive rules within a namespace; b) RoleBinding - grants namespace-wide access to k8s subjects and resources; c) ClusterRole - represents a set of additive rules within the cluster; d) ClusterRoleBinding - grants cluster-wide access to k8s subjects and resources.
The Traditional-approach:
You define three k8s users with different privileges in your cluster and
test them sequentially:
User type1 called pulumi:admin-usr
for users have cluster admin rights
$ cat user1.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: ClusterAdminRole
# no namespace needed
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: cluster-admin-binding
subjects:
- kind: User
name: "pulumi:admin-usr"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: ClusterAdminRole
apiGroup: rbac.authorization.k8s.io`
User type2 called pulumi:automation-usr
for users that have
permissions to all k8s resources in namespace automation. An e.g would
be your CI/CD pipeline
$ cat user2.yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: AutomationRole
namespace: automation
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: automation-binding
namespace: automation
subjects:
- kind: User
name: "pulumi:automation-usr"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: AutomationRole
apiGroup: rbac.authorization.k8s.io
User type 3 called prod-usr for users that have read access to all k8s resources in the namespace prod
$ cat user3.yaml
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: EnvProdRole
namespace: prod
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: env-prod-binding
namespace: prod
subjects:
- kind: User
name: "pulumi:prod-usr"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: EnvProdRole
apiGroup: rbac.authorization.k8s.io`
$ kubectl apply -f user1.yaml && kubectl apply -f user2.yaml && kubectl apply -f user3.yaml`
clusterrole.rbac.authorization.k8s.io/ClusterAdminRole created
clusterrolebinding.rbac.authorization.k8s.io/cluster-admin-binding created
role.rbac.authorization.k8s.io/AutomationRole created
rolebinding.rbac.authorization.k8s.io/automation created
role.rbac.authorization.k8s.io/EnvProdRole created
rolebinding.rbac.authorization.k8s.io/env-prod-binding created
The Pulumi-approach:
Update your index.ts
file with more code as follows:
/*
* 3) Single Step deployment of k8s RBAC configuration for user1, user2 and user3 per our example
*/
// Grant cluster admin access to all admins with k8s ClusterRole and ClusterRoleBinding
new k8s.rbac.v1.ClusterRole("clusterAdminRole", {
metadata: {
name: "clusterAdminRole",
},
rules: [{
apiGroups: ["*"],
resources: ["*"],
verbs: ["*"],
}]
}, {provider: cluster.provider});
new k8s.rbac.v1.ClusterRoleBinding("cluster-admin-binding", {
metadata: {
name: "cluster-admin-binding",
},
subjects: [{
kind: "User",
name: "pulumi:admin-usr",
}],
roleRef: {
kind: "ClusterRole",
name: "clusterAdminRole",
apiGroup: "rbac.authorization.k8s.io",
},
}, {provider: cluster.provider});
// User2 called automation-usr for users that have permissions to all k8s resources in the namespace automation
new k8s.rbac.v1.Role("AutomationRole", {
metadata: {
name: "AutomationRole",
namespace: "automation",
},
rules: [{
apiGroups: ["*"],
resources: ["*"],
verbs: ["*"],
}]
}, {provider: cluster.provider});
new k8s.rbac.v1.RoleBinding("automation-binding", {
metadata: {
name: "automation-binding",
namespace: "automation",
},
subjects: [{
kind: "User",
name: "pulumi:automation-usr",
apiGroup: "rbac.authorization.k8s.io",
}],
roleRef: {
kind: "Role",
name: "AutomationRole",
apiGroup: "rbac.authorization.k8s.io",
},
}, {provider: cluster.provider});
// User3 called prod-usr for users that have read access to all k8s resources in the namespace env-prod
new k8s.rbac.v1.Role("EnvProdRole", {
metadata: {
name: "EnvProdRole",
namespace: "prod",
},
rules: [{
apiGroups: ["*"],
resources: ["*"],
verbs: ["get", "watch", "list"],
}],
}, {provider: cluster.provider});
new k8s.rbac.v1.RoleBinding("env-prod-binding", {
metadata: {
name: "env-prod-binding",
namespace: "prod",
},
subjects: [{
kind: "User",
name: "pulumi:prod-usr",
apiGroup: "rbac.authorization.k8s.io",
}],
roleRef: {
kind: "Role",
name: "EnvProdRole",
apiGroup: "rbac.authorization.k8s.io",
},
}, {provider: cluster.provider});
export const kubeconfig = cluster.kubeconfig
Step 4: Update aws-iam-authenticator ConfigMap to map the IAM Roles to the Kubernetes usernames.
The Traditional-approach:
You get the three IAM role arns for clusterAdminRole
,
AutomationRole
, EnvProdRole
and update the configmap with k8s
usernames pulumi:admin-usr
, pulumi:automation-usr
and
pulumi:prod-usr
.
mapRoles:
----
- groups:
- system:masters
rolearn: arn:aws:iam::XXXXXXXXXXXX:role/clusterAdminRole
username: pulumi:admin-usr
- groups:
rolearn: arn:aws:iam::XXXXXXXXXXXX:role/AutomationRole
username: pulumi:automation-usr
- groups:
rolearn: arn:aws:iam::XXXXXXXXXXXX:role/EnvProdRole
username: pulumi:prod-usr
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::XXXXXXXXXXXX:role/eksctl-eks-rbac-nd-test-nodegroup-NodeInstanceRole-NP542EG8JX8U
username: system:node:`
The Pulumi-approach:
This step is not required as you have already updated the EKS ConfigMap
at cluster creation time in STEP 2 using “RoleMappings”. Simply run
pulumi up
with the full index.ts
file. Watch all your components
come alive simultaneously.
Setting up RBAC on one EKS cluster is a long convoluted sequential
process that requires multiple validations along the way. Imagine the
complexity involved when working with multiple tools for an environment
that requires multiple groups with many users, namespaces, and clusters.
Testing the Pulumi approach worked
Make sure you run pulumi up
with this index.ts
file.
$ pulumi stack output kubeconfig | jq > kubeconfig.yaml
$ export KUBECONFIG = kubeconfig.yaml
Assume the IAM role AutomationRole
with access to all Kubernetes
resources in namespace automation and test if the permissions work.
"users": [
{
"name": "aws",
"user": {
"exec": {
"apiVersion": "client.authentication.k8s.io/v1alpha1",
"args": [
"token",
"-i",
"eks-cluster-eksCluster-196b0de",
"-r",
"arn:aws:iam::xxxxxxxxxxxx:role/AutomationRole"
],
"command": "aws-iam-authenticator"
}
}
}
]
}
$ kubectl get po --namespace=automation
No resources found.
$ kubectl get po --namespace=prod
Error from server (Forbidden): pods is forbidden: User "pulumi: automation-usr" cannot list resource "pods" in API group "" in the namespace "prod"
Upon assuming the IAM role AutomationRole
which is mapped to
Kubernernetes username pulumi:automation-usr
in the EKS cluster
configmap, you are only restricted to the resources and verbs allowed in
the namespace “automation” and not in namespace “prod”.
Next Step
In this post, we discussed how setting up Kubernetes RBAC with Pulumi is simple, comprehensive, non-sequential and part of your everyday programming experience. You can find the complete pulumi code for our example and try it out yourself.
Pulumi is open source and free to use. For more examples, visit our GitHub examples page here. To learn more about Pulumi and how to manage Kubernetes through code, have a look at our “Get Started with Kubernetes” guide.