Skip to main content

Policy deployment and self governance with Spinnaker

Policy deployment and self governance with Spinnaker hero graphic

Feb 24, 2021 by Stephen Atwell

Giving developers direct access to production is great, but you want to ensure they stay safe. For example, allowing remote SSH access into an internet-accessible load balancer exposes a potential attack vector. To prevent this, I’m leveraging Armory’s Policy Engine for Spinnaker. Policy Engine enforces policies to govern what developers are allowed to do within Spinnaker, and can prevent load balancers exposing SSH. In addition to implementing the policy to govern Spinnaker, we will also use Spinnaker for our policy deployment.

 

Policy Engine reads policies from Open Policy Agent, which can read policies from ConfigMaps. We will deploy it this way, and then create a configuration map for each of our policies.
The policy deployment pipeline I’ll explain how to configure

The “Deploy OPA Service” stage deploys OPA. The other stages deploy the policies.

ouroboros: a snake eating its own tail
Spinnaker leverages Armory’s Policy Engine to query policies from OPA and enforces them against Spinnaker. Since I am using Spinnaker to deploy its own policies, I needed to secure control over policy deployment! To do this, I implemented two policies that restrict changes to the OPA deployment. It’s a bit of a snake eating its own tail.

Securing Policy Deployment via Policy

Since any configuration maps in the “opa” namespace can change policy,  I created a policy to ensure that only my OPA application can deploy to this namespace:

  package spinnaker.deployment.tasks.before.deployManifest
    deny[“Only the OPA application can deploy to the OPA namespace”]{
        deployToOpaNamespace
            input.deploy.moniker.app!=”opa”
    }
    deployToOpaNamespace{
            input.deploy.manifests[_].metadata.namespace==”opa”
    }

I also created a policy that ensures only I can edit pipelines for the OPA application:

    package spinnaker.execution.stages.before.savePipeline
   deny[“Only Stephen can change policy”] {
      input.pipeline.application==”opa”
     input.pipeline.authentication.user!=”stephenatwell”
    }
MyDeploy policy: protect policy pipeline” stage deploys both policies in two configuration maps. These policies provide error messages for other developers—here’s what it looks like if a different pipeline tries to deploy to my “opa” namespace:
other developers receive a friendly error message

Implementing a policy to Block SSH

After securing policy deployment, I moved on to implementing my policy. The stageDeploy Policy: Restrict Port 22″ deploys the policy that prevents my developers from exposing port 22 in production load balancers:
package spinnaker.deployment.tasks.deployManifest
deny[“LoadBalancer Services must not have port 22 open.”] {
    manifests := input.deploy.manifests
    manifest := manifests[_]
    manifest.kind == “Service”
    manifest.spec.type == “LoadBalancer”
    port := manifest.spec.ports[_]
    port.port == 22
}
In the future, I can easily add new policies as additional stages in my OPA pipeline. If you are interested in leveraging Spinnaker to deploy an Open Policy Agent, or its policies, an export of my deployment pipeline can be found below:

 

Full pipeline export:
{
“id”: “07ebad32-3083-417d-9c06-c81025146058”,
“metadata”: {
“description”: “A pipeline to deploy OPA that enforces Spinnaker policies”,
“name”: “Ouroboros”,
“owner”: “[email protected]”,
“scopes”: [
“global”
]
},
“pipeline”: {
“keepWaitingPipelines”: false,
“lastModifiedBy”: “stephenatwell”,
“limitConcurrent”: true,
“spelEvaluator”: “v4”,
“stages”: [
{
“account”: “spinnaker”,
“cloudProvider”: “kubernetes”,
“manifests”: [
{
“apiVersion”: “apps/v1”,
“kind”: “Deployment”,
“metadata”: {
“labels”: {
“app”: “opa”
},
“name”: “opa-deployment”,
“namespace”: “opa”
},
“spec”: {
“progressDeadlineSeconds”: 600,
“replicas”: 1,
“revisionHistoryLimit”: 10,
“selector”: {
“matchLabels”: {
“app”: “opa”
}
},
“strategy”: {
“rollingUpdate”: {
“maxSurge”: “25%”,
“maxUnavailable”: “25%”
},
“type”: “RollingUpdate”
},
“template”: {
“metadata”: {
“labels”: {
“app”: “opa”
}
},
“spec”: {
“containers”: [
{
“args”: [
“run”,
“–server”,
“–addr=http://0.0.0.0:8181”,
“–log-level”,
“debug”
],
“image”: “openpolicyagent/opa:0.17.2”,
“imagePullPolicy”: “IfNotPresent”,
“livenessProbe”: {
“failureThreshold”: 3,
“httpGet”: {
“path”: “/health”,
“port”: 8181,
“scheme”: “HTTP”
},
“initialDelaySeconds”: 3,
“periodSeconds”: 5,
“successThreshold”: 1,
“timeoutSeconds”: 1
},
“name”: “opa”,
“readinessProbe”: {
“failureThreshold”: 3,
“httpGet”: {
“path”: “/health”,
“port”: 8181,
“scheme”: “HTTP”
},
“initialDelaySeconds”: 3,
“periodSeconds”: 5,
“successThreshold”: 1,
“timeoutSeconds”: 1
},
“resources”: {},
“terminationMessagePath”: “/dev/termination-log”,
“terminationMessagePolicy”: “File”
},
{
“args”: [
“–policies=opa”,
“–require-policy-label=true”
],
“image”: “openpolicyagent/kube-mgmt:0.9”,
“imagePullPolicy”: “IfNotPresent”,
“name”: “kube-mgmt”,
“resources”: {},
“terminationMessagePath”: “/dev/termination-log”,
“terminationMessagePolicy”: “File”
}
],
“dnsPolicy”: “ClusterFirst”,
“restartPolicy”: “Always”,
“schedulerName”: “default-scheduler”,
“securityContext”: {},
“terminationGracePeriodSeconds”: 30
}
}
}
}
],
“moniker”: {
“app”: “opa”
},
“name”: “Deploy OPA Service”,
“refId”: “1”,
“requisiteStageRefIds”: [],
“skipExpressionEvaluation”: false,
“source”: “text”,
“trafficManagement”: {
“enabled”: false,
“options”: {
“enableTraffic”: false,
“services”: []
}
},
“type”: “deployManifest”
},
{
“account”: “spinnaker”,
“cloudProvider”: “kubernetes”,
“manifests”: [
{
“apiVersion”: “v1”,
“data”: {
“restrict-opa-app.rego”: “package spinnaker.execution.stages.before.savePipeline\ndeny[\”Only Stephen can change policy\”] {\n input.pipeline.application==\”opa\”\n input.pipeline.authentication.user!=\”stephenatwell\”\n}\n”
},
“kind”: “ConfigMap”,
“metadata”: {
“labels”: {
“openpolicyagent.org/policy”: “rego”
},
“name”: “restrict-opa-app”,
“namespace”: “opa”
}
},
{
“apiVersion”: “v1”,
“data”: {
“restrict-opa-namespace.rego”: “package spinnaker.deployment.tasks.before.deployManifest\ndeny[\”Only the OPA application can deploy to the OPA namespace\”]{\n deployToOpaNamespace \n input.deploy.moniker.app!=\”opa\” \n}\ndeployToOpaNamespace{\n input.deploy.manifests[_].metadata.namespace==\”opa\”\n}\n”
},
“kind”: “ConfigMap”,
“metadata”: {
“labels”: {
“openpolicyagent.org/policy”: “rego”
},
“name”: “restrict-opa-namespace”,
“namespace”: “opa”
}
}
],
“moniker”: {
“app”: “opa”
},
“name”: “Deploy Policy: protect policy pipeline”,
“refId”: “4”,
“requisiteStageRefIds”: [
“1”
],
“skipExpressionEvaluation”: false,
“source”: “text”,
“trafficManagement”: {
“enabled”: false,
“options”: {
“enableTraffic”: false,
“services”: []
}
},
“type”: “deployManifest”
},
{
“account”: “spinnaker”,
“cloudProvider”: “kubernetes”,
“manifests”: [
{
“apiVersion”: “v1”,
“data”: {
“prevent-ssh-on-load-balancers.rego”: “package spinnaker.deployment.tasks.deployManifest\n\ndeny[\”LoadBalancer Services must not have port 22 open.\”] {\n manifests := input.deploy.manifests\n manifest := manifests[_]\n manifest.kind == \”Service\”\n manifest.spec.type == \”LoadBalancer\”\n port := manifest.spec.ports[_]\n port.port == 22\n}\n”
},
“kind”: “ConfigMap”,
“metadata”: {
“labels”: {
“openpolicyagent.org/policy”: “rego”
},
“name”: “prevent-ssh-on-load-balancers”,
“namespace”: “opa”
}
}
],
“moniker”: {
“app”: “opa”
},
“name”: “Deploy Policy: Restrict Port 22”,
“refId”: “5”,
“requisiteStageRefIds”: [
“1”
],
“skipExpressionEvaluation”: false,
“source”: “text”,
“trafficManagement”: {
“enabled”: false,
“options”: {
“enableTraffic”: false,
“services”: []
}
},
“type”: “deployManifest”
}
],
“triggers”: [],
“updateTs”: “1614119729000”
},
“protect”: false,
“schema”: “v2”,
“variables”: []
}

Recently Published Posts

July 26, 2021
|
by Phebe Vickers

A day in the life of a TAM

I’ve been asked what a Technical Account Manager (TAM) does so I wanted to take the opportunity to illustrate it by walking through a standard day in the life. Before we can look at what a day in a life of a TAM is, I should provide some background in what is a TAM and […]

Read more

June 29, 2021
|
by Nikema Prophet

Nikema’s Spinnaker Summit 2021 Recap

My Second Spinnaker Summit is in the Books! Last week I attended and spoke at my second Spinnaker Summit. Like last year’s summit, it was fully virtual. This time Spinnaker Summit was co-located with cdCon and took place on the Hopin platform. Last year, I spoke on a panel about Black professionals a few months […]

Read more

June 28, 2021
|
by Stephen Atwell

Announcing General Availability of Armory Policy Engine Plugin

Armory Policy Engine provides support for automating policy compliance with Spinnaker. Policy Engine Plugin is the latest version of Policy Engine and adds support for both advanced role-based access control (RBAC) use-cases and open source Spinnaker. The release of Policy Engine Plugin comes with new documentation, including a library of example policies from across Armory’s […]

Read more