Table of Contents
We're very excited to announce our new open source tool for managing policies in Kubernetes called jsPolicy. Watch this YouTube video for more info and a demo, or read the transcript below if you prefer.
{{< youtube s7dt27pMchk >}}
Video transcript
Hi, I'm Rich with Loft Labs and I'm here to share with you another exciting open source tool that we've released for Kubernetes called jsPolicy. Policies are a hot topic in the Kubernetes community and for good reason. Many people operate Kubernetes clusters in regulated environments, and they're subject to audits like PCI-DSS, HIPAA or SOC 2. And even if you don't deal with external auditors, you may have internal security audits, or you may just want to enforce policy for your own peace of mind.
Up until now the main option for enforcing policies in Kubernetes has been Open Policy Agent. OPA is great, and people have built a lot of cool things with it. But here at Loft, we've been hearing from users who were having trouble with writing and maintaining policies long-term. OPA is complex and uses its own language called Rego which isn't intuitive for everyone. We believe that platform engineers deserve the same high quality workflows, testing frameworks, and easy to reuse libraries as backend and front end engineers.
That's why we built jsPolicy, a new open source tool that uses JavaScript to define policies. Why JavaScript? Well, it's everywhere. If you've worked in tech, you've likely run into it at some point. If you're on a DevOps or platform team, you're likely not a JavaScript expert, but you can probably look at policies written in JavaScript and understand them without hassle.
With JavaScript, we're also able to take advantage of Google's V8 JavaScript engine. The V8 engine is used in Chrome in many other projects, and it's been highly optimized for over a decade. Our goal for jsPolicy was to develop a simple, maintainable, and easy to understand framework for teams to write and administer policies for Kubernetes.
Let's take a look now at how jsPolicy works.
For this demo, I'm using a local Kubernetes cluster running with Docker Desktop, but you can use jsPolicy with any Kubernetes cluster.
Here's the GitHub repo for jsPolicy. The instructions are in the README.
jsPolicy comes with a number of example policies broken out by use cases and jsPolicy features. We're going to use one of the example policies in the file called deny-default-namespace.yaml. Let's have a quick look at the code.
apiVersion: policy.jspolicy.com/v1beta1
kind: JsPolicy
metadata:
name: "deny-default-namespace.example.com"
spec:
operations: ["CREATE"]
resources: ["*"]
scope: Namespaced
javascript: |
if (request.namespace === "default") {
deny("Creation of resources within the default namespace is not allowed!");
}
As you can see, this is very straightforward. I don't know a lot of JavaScript myself, but when I saw this policy for the first time I understood immediately what was happening
This is an admission control policy. When new resources are created we check the namespace. If it's set to default we deny the action and return the deny message. That namespace could be a different one than the default namespace or even an array of multiple namespaces. Okay. Let's get to a shell and see how this works.
We install jsPolicy using Helm.
$ helm install jspolicy jspolicy -n jspolicy --create-namespace --repo https://charts.loft.sh
NAME: jspolicy
LAST DEPLOYED: Wed May 12 13:24:54 2021
NAMESPACE: jspolicy
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing jspolicy.
Your release is named jspolicy.
To learn more about the release, try:
$ helm status jspolicy -n jspolicy
$ helm get all jspolicy -n jspolicy
Learn more about using jsPolicy here: https://github.com/loft-sh/jspolicy
I'm in the examples/by-use-case directory and there's our deny-default-namespace.yaml file. Let's apply that.
$ cd examples/by-use-case
$ kubectl apply -f deny-default-namespace.yaml
jspolicy.policy.jspolicy.com/deny-default-namespace.example.com created
You can see that we've created a policy. We can see the details of it with kubectl get or describe.
$ kubectl get jspolicy
NAME AGE
deny-default-namespace.example.com 47s
$ kubectl get jspolicy -o yaml
apiVersion: v1
items:
- apiVersion: policy.jspolicy.com/v1beta1
kind: JsPolicy
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"policy.jspolicy.com/v1beta1","kind":"JsPolicy","metadata":{"annotations":{},"name":"deny-default-namespace.example.com"},"spec":{"javascript":"if (request.namespace === \"default\") {\n deny(\"Creation of resources within the default namespace is not allowed!\");\n}\n","operations":["CREATE"],"resources":["*"],"scope":"Namespaced"}}
creationTimestamp: "2021-05-12T20:26:07Z"
generation: 1
name: deny-default-namespace.example.com
resourceVersion: "1264"
selfLink: /apis/policy.jspolicy.com/v1beta1/jspolicies/deny-default-namespace.example.com
uid: 4d654623-679a-418f-b774-299d137cf69b
spec:
javascript: |
if (request.namespace === "default") {
deny("Creation of resources within the default namespace is not allowed!");
}
operations:
- CREATE
resources:
- '*'
scope: Namespaced
status:
bundleHash: ee713659c76132fa9fd5bd0218e2f7703afd79d71cccfb338bf6622e17f5b37c
phase: Synced
kind: List
metadata:
resourceVersion: ""
selfLink: ""
And there's our policy.
jsPolicy also takes this policy code and runs it through webpack, which first embeds all libraries used in the policy, then optimizes the code, and finally creates a minified JavaScript bundle. We can view the highly optimized version of our policy using kubectl.
$ kubectl get jspolicybundle -o yaml
apiVersion: v1
items:
- apiVersion: policy.jspolicy.com/v1beta1
kind: JsPolicyBundle
metadata:
creationTimestamp: "2021-05-12T20:26:10Z"
generation: 1
name: deny-default-namespace.example.com
ownerReferences:
- apiVersion: policy.jspolicy.com/v1beta1
blockOwnerDeletion: true
controller: true
kind: JsPolicy
name: deny-default-namespace.example.com
uid: 4d654623-679a-418f-b774-299d137cf69b
resourceVersion: "1262"
selfLink: /apis/policy.jspolicy.com/v1beta1/jspolicybundles/deny-default-namespace.example.com
uid: 3e5aa437-7184-462f-8705-9eb7801c7db6
spec:
bundle: H4sIAAAAAAAA/0SNwUoDMRRFf+U1iyGBMrg2xI1r0X0p5Tm5QyMxGV9eWssw/y4F0e3lnnOsdeFpvbAQwrp5EzFzz2pCCIKvjqZj4U+0hScMQ0S5WfMsYE21UJ1J0GqXCY2uSc+pkJ5BvxL6Iyk1KlWJc65XxJ1x/p7kgO+lijY/V7H3RSgVguODHAMOcvQYTye0lxp7xjC8vn9g0jFiTgVvUheI3izvzf/J7NcL547H3cPmNmed/wEAAP//AQAA//9BhjR34wAAAA==
kind: List
metadata:
resourceVersion: ""
selfLink: ""
Now let's try to spin up something in the default namespace to see the policy in action. I have a very simple Nginx deployment YAML here that runs two Nginx pods.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19.10
ports:
- containerPort: 80
Let's try to start it up in the default namespace.
$ kubectl apply -f nginx.yaml
Error from server (Forbidden): error when creating "nginx.yaml": admission webhook "deny-default-namespace.example.com" denied the request: Creation of resources within the default namespace is not allowed!
And there we go. Our JavaScript policy has looked at that new object being created and denied the action.
So let's create a new namespace for Nginx and launch our deployment there instead.
$ kubectl create namespace nginx
namespace/nginx created
$ kubectl apply -f nginx.yaml -n nginx
deployment.apps/nginx-deployment created
That works as expected.
Of course using any programming language inside a YAML file isn't ideal. So with jsPolicy, you can use our TypeScript starter project to write policies. With TypeScript, you can import the official Kubernetes TypeScript definitions and get types and auto-completion while writing your policies.
If you save a policy, the jsPolicy Webpack plugin will create your JavaScript bundles and spit out the YAML files that you can apply to your cluster, or version with Git with your familiar workflow. You can check out the jsPolicy policy TypeScript starter project on GitHub and benefit from great dev workflows, testing frameworks and other libraries available in the TypeScript and JavaScript ecosystem.
That's a quick look at jsPolicy. As you can see, it's very intuitive and fast and it will make your policies better, understandable, testable, and maintainable. Even if you're not a JavaScript expert, it's not hard to read and write policies. And there are a ton of resources on the internet for learning JavaScript.
Also the product engineers that you work with probably know some JavaScript, which means they can look at policies and understand why something they tried was denied. Application engineers could even write their own policies. And because all of this policy logic is now written in JavaScript or JS-compatible languages such as TypeScript, you can publish your policy code and share it via npmjs, GitHub, or in your own private npm registry. We've published an npm package with over 50 customizable policies for common use cases that you can use out of the box, including a more customizable version of our deny namespace policy that we saw in the demo.
We're very excited about jsPolicy and we're looking forward to seeing what people do with it. We built jsPolicy because our Loft customers were asking us for a simpler solution for managing policies, but we've open sourced it so people who aren't Loft customers can benefit too. If you're interested in learning more head over to the jsPolicy GitHub page and try out some of the examples. It's github.com/loft-sh/jspolicy. Or check out the website on jspolicy.com.