Skip to main content

Policy deployment and self governance with Spinnaker

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”: []
}