Policy deployment and self governance with Spinnaker hero graphic

Policy deployment and self governance with Spinnaker

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

Recently Published Posts

Enforce Least Privilege & Separation of Duties during Continuous Deployment

Feb 2, 2023

Role-based access controls enforce the principles of least privilege and separation of duties in your continuous deployment pipeline to simplify SOC2.

Read more

Argo + Armory: Cross-environment orchestration made easy

Feb 1, 2023

Cross-environment orchestration that you don’t have to spend time building At Armory, our goal is software innovation, whether that’s our own Continuous Deployment solutions, or being able to help our customers reach higher innovation peaks within their software development. We’ve taken deliberate steps to make sure our products play well with others, with a focus […]

Read more

Navigating AWS Deployment Targets with Armory

Jan 20, 2023

Many organizations look to Amazon Web Services (AWS) to host and deploy their applications in the cloud. However, they’re finding that their deployment tooling, often built as an extension of their legacy continuous integration (CI), is one of the main impediments to adopting cloud services.  Custom-scripted production pipelines built with in-house tooling need to be […]

Read more