diff --git a/README.md b/README.md index f8c4f065..aa21e73e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ For all Cloud Run code samples, see the [Cloud Run sample browser](https://cloud | gcloud as a service | Use `gcloud` and `gsutil` in a service | [Go][gcloud_report] | | VPC Testing | Egress and ingress settings with VPC | [Python][vpc_sample] | | Multi-container Hello Nginx| Multi-containers with nginx | [YAML][multicontainer_hello_nginx_sample] | +| Open Policy Agent | Open Policy Agent Multi-container | [YAML][multicontainer_open_policy_agent_sample] | | Markdown Preview | 2 tier secure microservices for Markdown rendering | [Go][markdown_preview_go], [Nodejs][markdown_preview_nodejs], [Python][markdown_preview_python], [Java][markdown_preview_java] | [job_go]: https://github.com/GoogleCloudPlatform/golang-samples/tree/main/run/jobs @@ -87,6 +88,7 @@ For all Cloud Run code samples, see the [Cloud Run sample browser](https://cloud [vpc_sample]: vpc-sample [gcloud_report]: gcloud-report [multicontainer_hello_nginx_sample]: multi-container/hello-nginx-sample +[multicontainer_open_policy_agent_sample]: multi-container/open-policy-agent-sample [idtoken_request_go]: https://github.com/GoogleCloudPlatform/golang-samples/blob/master/functions/security/idtoken.go [idtoken_request_nodejs]: https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-serverless.js [idtoken_request_python]: https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/auth/service-to-service/auth.py diff --git a/multi-container/open-policy-agent-sample/Dockerfile.nginx b/multi-container/open-policy-agent-sample/Dockerfile.nginx new file mode 100644 index 00000000..48661ae3 --- /dev/null +++ b/multi-container/open-policy-agent-sample/Dockerfile.nginx @@ -0,0 +1,5 @@ +FROM nginx:1.20.0-alpine + +RUN mkdir /etc/nginx/templates && mkdir /usr/share/nginx/html/bundles +COPY nginx.conf.template /etc/nginx/templates +COPY bundle.tar.gz /usr/share/nginx/html/bundles \ No newline at end of file diff --git a/multi-container/open-policy-agent-sample/README.md b/multi-container/open-policy-agent-sample/README.md new file mode 100644 index 00000000..e6180bd4 --- /dev/null +++ b/multi-container/open-policy-agent-sample/README.md @@ -0,0 +1,50 @@ +# Running OPA as a side car in Cloud Run + +In this tutorial, we will use the [same example](http://openpolicyagent.org/docs/latest/http-api-authorization/) that is documented in the official OPA website, but substitute using [Cloud Run side cars](https://cloud.google.com/run/docs/deploying#sidecars) instead of Docker Compose. + +We'll end up with a deployment like this: + +![deployment diagram](cloud-run-opa-deployment-diagram.png) + +The goal is to use a simple HTTP web server that accepts any HTTP GET request that you issue and echoes the OPA decision back as text. OPA will fetch policy bundles from a simple bundle server. OPA, the bundle server, and the web server will be run as containers in Cloud Run. In a production environment, you likely will have a different bundle server, but it's convenient to package it together here. + +# Prerequsites + +* An up to date gcloud SDK installed +* [opa](https://www.openpolicyagent.org/docs/latest/) installed +* A text editor +* a Google Cloud project with the Cloud Run API enabled + +# 1. Create and push a custom bundle server image + +* Follow [Step 1](https://www.openpolicyagent.org/docs/latest/http-api-authorization/#1-create-a-policy-bundle) in the original tutorial, resulting in creating two files called `example.rego` and `bundle.tar.gz`. +* Create a custom bundle server with [Dockerfile.nginx](./Dockerfile.nginx) and [nginx.conf.template](./nginx.conf.template) with the following command: + +```bash +docker build -f Dockerfile.nginx -t us-central1-docker.pkg.dev//docker/ . + +docker push us-central1-docker.pkg.dev//docker/ +``` + +Note: We assume Google Cloud Artifact Registry in this tutorial but any registry accessible to Cloud Run will work + +# 2. Update opa-service.yml + +Read through [Step 2](https://www.openpolicyagent.org/docs/latest/http-api-authorization/#2-bootstrap-the-tutorial-environment-using-docker-compose) in the original tutorial, but instead of creating a `docker-compose.yml` file and using `docker-compose` to create the service, we're instead going to use `opa-service.yaml` [reference docs](https://cloud.google.com/run/docs/reference/yaml/v1) to deploy three containers to a single cloud run deployment. + +* Update the bundle server image path ([line 39](./opa-service.yml#39)) to the path of your custom image above. +* use gcloud to deploy: + +```bash +gcloud beta run services replace opa-service.yaml +``` + +# 3. Verify + +Follow [steps 3-5](https://www.openpolicyagent.org/docs/latest/http-api-authorization/#3-check-that-alice-can-see-her-own-salary) in the original tutorial to verify the policies with one small change: + + any time you see `localhost:5000` replace it with the Cloud Run URL. For e.g. + +```bash +curl --user alice:password /finance/salary/alice +``` diff --git a/multi-container/open-policy-agent-sample/cloud-run-opa-deployment-diagram.png b/multi-container/open-policy-agent-sample/cloud-run-opa-deployment-diagram.png new file mode 100644 index 00000000..1e08aad6 Binary files /dev/null and b/multi-container/open-policy-agent-sample/cloud-run-opa-deployment-diagram.png differ diff --git a/multi-container/open-policy-agent-sample/nginx.conf.template b/multi-container/open-policy-agent-sample/nginx.conf.template new file mode 100644 index 00000000..40e52d37 --- /dev/null +++ b/multi-container/open-policy-agent-sample/nginx.conf.template @@ -0,0 +1,15 @@ +server { + listen ${NGINX_PORT}; + server_name _; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} \ No newline at end of file diff --git a/multi-container/open-policy-agent-sample/opa-service.yaml b/multi-container/open-policy-agent-sample/opa-service.yaml new file mode 100644 index 00000000..49276cee --- /dev/null +++ b/multi-container/open-policy-agent-sample/opa-service.yaml @@ -0,0 +1,49 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + annotations: + run.googleapis.com/launch-stage: BETA + name: "SERVICE_NAME" + labels: + cloud.googleapis.com/location: "REGION" +spec: + template: + metadata: + annotations: + run.googleapis.com/container-dependencies: '{"opa":["bundle-server"], "api-server":["opa"] }' + spec: + containers: + - image: "openpolicyagent/demo-restful-api:0.3" + name: api-server + env: + - name: "OPA_ADDR" + value: "http://localhost:8181" #opa agent + - name: "POLICY_PATH" + value: "/v1/data/httpapi/authz" + startupProbe: + tcpSocket: + port: 5000 + ports: + - containerPort: 5000 + - image: "openpolicyagent/opa:0.55.0" + name: opa + command: + - "opa" + - "run" + - "--server" + - "--log-format=json-pretty" + - "--set=decision_logs.console=true" + - "--set=services.nginx.url=http://localhost:8888" #bundle_server + - "--set=bundles.nginx.service=nginx" + - "--set=bundles.nginx.resource=bundles/bundle.tar.gz" + startupProbe: + tcpSocket: + port: 8181 + - image: "us-central1-docker.pkg.dev//docker/" + name: bundle-server + env: + - name: "NGINX_PORT" + value: "8888" + startupProbe: + tcpSocket: + port: 8888 diff --git a/multi-container/open-policy-agent-sample/tests.cloudbuild.yaml b/multi-container/open-policy-agent-sample/tests.cloudbuild.yaml new file mode 100644 index 00000000..b0af08ae --- /dev/null +++ b/multi-container/open-policy-agent-sample/tests.cloudbuild.yaml @@ -0,0 +1,93 @@ +steps: + - id: "create-example-opa-bundle" + waitFor: ["-"] + name: 'gcr.io/gcp-runtimes/ubuntu_18_0_4' + dir: "${_SAMPLE_DIR}" + script: | + #!/bin/bash + cat <> example.rego + package httpapi.authz + + # bob is alice's manager, and betty is charlie's. + subordinates := {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} + + default allow := false + + # Allow users to get their own salaries. + allow { + input.method == "GET" + input.path == ["finance", "salary", input.user] + } + + # Allow managers to get their subordinates' salaries. + allow { + some username + input.method == "GET" + input.path = ["finance", "salary", username] + subordinates[input.user][_] == username + } + EOF + cat example.rego + + #get opa binary and build + curl -L -o opa https://openpolicyagent.org/downloads/v0.55.0/opa_linux_amd64_static + chmod a+x ./opa + ./opa build example.rego -o bundle.tar.gz + + - id: "build-bundle-server" + waitFor: ["create-example-opa-bundle"] + name: 'gcr.io/cloud-builders/docker' + dir: "${_SAMPLE_DIR}" + args: ['build', '-f', 'Dockerfile.nginx', '-t', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker/opa-sample-test', '.'] + + - id: "push-bundle-server" + waitFor: ["build-bundle-server"] + name: 'gcr.io/cloud-builders/docker' + args: ['push', 'us-central1-docker.pkg.dev/$PROJECT_ID/docker/opa-sample-test'] + + - id: "e2e-test" + waitFor: ["push-bundle-server"] + name: "gcr.io/cloud-builders/gcloud" + dir: "${_SAMPLE_DIR}" + script: | + #!/bin/bash + echo "Run e2e-test ..." + ./tests/e2e-test.sh || touch /workspace/e2e-failed + env: + - "BUILD_ID=$BUILD_ID" + - "_REGION=$_REGION" + - "_SERVICE_NAME=$_SERVICE_NAME" + - "PROJECT_ID=$PROJECT_ID" + - "IMAGE_NAME=opa-sample-test" + + - id: "e2e-cleanup" + waitFor: ["e2e-test"] + name: "gcr.io/cloud-builders/gcloud" + dir: "${_SAMPLE_DIR}" + script: | + #!/bin/bash + export MC_SERVICE_NAME="${_SERVICE_NAME}-$BUILD_ID" + gcloud run services delete "${MC_SERVICE_NAME}" --quiet --region "${_REGION}" + echo gcloud artifacts docker images delete us-central1-docker.pkg.dev/${PROJECT_ID}/docker/opa-sample-test --quiet --project ${PROJECT_ID} + gcloud artifacts docker images delete us-central1-docker.pkg.dev/${PROJECT_ID}/docker/opa-sample-test --quiet --project ${PROJECT_ID} + env: + - "BUILD_ID=$BUILD_ID" + - "_REGION=$_REGION" + - "_SERVICE_NAME=$_SERVICE_NAME" + - "IMAGE_NAME=opa-bundle" + - "PROJECT_ID=$PROJECT_ID" + + - id: "report-status" + waitFor: ["e2e-cleanup"] + name: "gcr.io/cloud-builders/gcloud" + script: | + #!/bin/bash + if [[ -f /workspace/e2e-failed ]] + then + echo "Step e2e-test failed" + exit 1 + fi +substitutions: + _SERVICE_NAME: "opa-sample-testing" + _REGION: "us-central1" + _SAMPLE_DIR: "multi-container/open-policy-agent-sample" diff --git a/multi-container/open-policy-agent-sample/tests/e2e-test.sh b/multi-container/open-policy-agent-sample/tests/e2e-test.sh new file mode 100755 index 00000000..b7b22203 --- /dev/null +++ b/multi-container/open-policy-agent-sample/tests/e2e-test.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eux pipefail + +export SERVICE_NAME="${_SERVICE_NAME}-$BUILD_ID" + +# Substituting the env vars in cloud run yaml file +sed -i -e s/SERVICE_NAME/${SERVICE_NAME}/g -e s/REGION/${_REGION}/g -e s/\/${PROJECT_ID}/g -e s/\/${IMAGE_NAME}/g opa-service.yaml + +# Note that nginx_config secret has already been created within project. +# Deploy multi-container service "nginx-example" that includes nginx proxy. +gcloud run services replace opa-service.yaml --region ${_REGION} --quiet + +# Wait till deployment completes +sleep 10 + +# Retrieve multi-containter service url. +URL=$(gcloud run services describe ${SERVICE_NAME} --region ${_REGION} --format 'value(status.url)') + +# Retrieve service deployment status. +STATUS=$(gcloud run services describe ${SERVICE_NAME} --region ${_REGION} --format 'value(status.conditions[0].type)') + +if [[ -z "${URL}" && "${STATUS}" != "Ready" ]] +then + echo "No Cloud Run opa sample url found. Step e2e-test failed." + exit 1 +fi + +#allow all users +gcloud run services add-iam-policy-binding ${SERVICE_NAME} --member=allUsers --role roles/run.invoker --region ${_REGION} + +# check that it's responding at all +RESULT=`curl ${URL}` + +if [[ $RESULT != *"Error: user Anonymous is not authorized to GET url /"* ]]; then + echo "No Cloud Run opa sample found deployed. Step e2e-test failed." + exit 1 +fi + + +RESULT=`curl --user alice:password ${URL}/finance/salary/alice` +echo $RESULT +if [[ $RESULT != *"Success: user alice is authorized"* ]]; then + echo "opa not functioning properly. Step e2e-test failed." + exit 1 +fi + +echo "Cloud Run opa sample successully deployed and nginx successfully proxied request." +exit 0 \ No newline at end of file