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

October 20, 2021
|
by Jason McIntosh

Monitoring Spinnaker: Part 1

Overview One of the questions that comes up a lot is how you monitor Spinnaker itself.  Not the apps Spinnaker is deploying, but Spinnaker itself and how it’s performing.  This is a question that has a lot of different answers. There are a few guidelines, but many of the answers are the same as how […]

Read more

October 18, 2021
|
by David Morgenthaler

The Importance of Patents: Interview with Nick Petrella, Head of Legal

    In honor of Armory’s recent acquisition of a patent for continuous software deployment, we sat down with Nick Petrella, Head of Legal, for a casual conversation covering a wide range of subjects, from patent law to Nick’s background as a software engineer and why he made the leap to the law. Check out […]

Read more

October 11, 2021
|
by Carl Timm

Armory Agent for Kubernetes Simplifies K8s Complexity

I’ll be upfront with you, I’m a sucker for a good origin story. It’s one of the reasons I spent hours engrossed in the Marvel Cinematic Universe not too long ago.  Rooting for incredibly flawed individuals with an outsized sense of duty and superpowers to back it up….What’s not to love?  My partner has a […]

Read more