Binary authorization module and example

This commit is contained in:
Miren Esnaola 2022-06-17 15:58:54 +02:00
parent 6c63c6aed8
commit 2e9fdea1a4
30 changed files with 3637 additions and 1 deletions

4
.gitignore vendored
View File

@ -28,4 +28,6 @@ fast/stages/**/terraform-*.auto.tfvars.json
fast/stages/**/0*.auto.tfvars*
**/node_modules
fast/stages/**/globals.auto.tfvars.json
cloud_sql_proxy
cloud_sql_proxy
examples/cloud-operations/binauthz/tenant-setup.yaml
examples/cloud-operations/binauthz/app/app.yaml

View File

@ -0,0 +1,127 @@
# Binary Authorization
The following example shows to how to create a CI and a CD pipeline in Cloud Build for the deployment of an application to a private GKE cluster with unrestricted access to a public endpoint. The example enables a Binary Authorization policy in the project so only images that have been attested can be deployed to the cluster. The attestations are created using a cryptographic key pair that has been provisioned in KMS.
The diagram below depicts the architecture used in the example.
![Architecture](diagram.png)
The CI and CD pipelines are implemented as Cloud Build triggers that run with a user-specified service account.
The CI pipeline does the following:
* Builds and pushes the image to Artifact registry
* Creates an attestation for the image.
The CD pipeline deploys the application to the cluster.
## Running the example
Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=examples%2Fcloud-operations%2Fbinauthz), then go through the following steps to create resources:
* `terraform init`
* `terraform apply -var project_id=my-project-id`
WARNING: The example requires the activation of the Binary Authorization API. That API does not support authentication with user credentials. A service account will need to be used to run the example
## Testing the example
Once the resources have been created, do the following to verify that everything works as expected.
1. Fetch the cluster credentials
gcloud container clusters get-credentials cluster --project <PROJECT_ID>
2. Apply the manifest tenant-setup.yaml available in your work directory.
kubectl apply -f tenant-setup.yaml
By applying that manifest thw following is created:
* A namespace called "apis". This is the namespace where the application will be deployed.
* A Role and a RoleBinding in previously created namespace so the service account that has been configured for the CD pipeline trigger in Cloud Build is able to deploy the kubernetes application to that namespace.
3. Change to the image subdirectory in your work directory
cd <WORK_DIR>/image
4. Run the following commands:
git init
git remote add origin ssh://<USER>:2022/p/<PROJECT_ID>/r/image
git push -u origin main
4. In the Cloud Build > History section in the Google Cloud console you should see a job running. That job is build the image, pushing to Artifact Registry and creating an attestation.
Once the job finishes copy the digest of the image that is displayed in the Cloud Build job output.
5. Change to the app subdirectory in your working directory.
cd <WORK_DIR>/app
6. Edit the app.yaml file and replace the string DIGEST with the value you copied before.
7. Run the following commands:
git init
git remote add origin ssh://<USER>:2022/p/<PROJECT_ID>/r/app
git push -u origin main
8. In the Cloud Build > History section in the Google Cloud console you should see a job running. The job will deploy the application to the cluster.
9. Go to the Kubernetes Engine > Workloads section to check that the deployment was successful and that the Binary Authorization admissions controller webhook did not block the deployment.
10. Change to the working directory and try to deploy an image that has not been attested.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: gcr.io/google-containers/nginx:latest
ports:
- containerPort: 80
EOF
9. Go to the Kubernetes Engine > Workloads section to check that that the Binary Authorization admissions controller webhook did not block the deployment.
The application deployed to the cluster is an RESTful API that enables managing Google Cloud storage buckets in the project. Workload identity is used so the app can interact with the Google Cloud Storage API.
Once done testing, you can clean up resources by running `terraform destroy`.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L26) | Project ID. | <code>string</code> | ✓ | |
| [master_cidr_block](variables.tf#L49) | Master CIDR block. | <code>string</code> | | <code>&#34;10.0.0.0&#47;28&#34;</code> |
| [pods_cidr_block](variables.tf#L37) | Pods CIDR block. | <code>string</code> | | <code>&#34;172.16.0.0&#47;20&#34;</code> |
| [prefix](variables.tf#L31) | Prefix for resources created. | <code>string</code> | | <code>null</code> |
| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [region](variables.tf#L61) | Region. | <code>string</code> | | <code>&#34;europe-west1&#34;</code> |
| [services_cidr_block](variables.tf#L43) | Services CIDR block. | <code>string</code> | | <code>&#34;192.168.0.0&#47;24&#34;</code> |
| [subnet_cidr_block](variables.tf#L55) | Subnet CIDR block. | <code>string</code> | | <code>&#34;10.0.1.0&#47;24&#34;</code> |
| [zone](variables.tf#L67) | Zone. | <code>string</code> | | <code>&#34;europe-west1-c&#34;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [app_repo_url](outputs.tf#L22) | App source repository url. | |
| [image_repo_url](outputs.tf#L17) | Image source repository url. | |
<!-- END TFDOC -->

View File

@ -0,0 +1,26 @@
# Copyright 2022 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
#
# https://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.
steps:
- id: 'Deploy app'
name: 'gcr.io/cloud-builders/kubectl'
args:
- 'apply'
- '-f'
- 'app.yaml'
env:
- 'CLOUDSDK_COMPUTE_ZONE=${_ZONE}'
- 'CLOUDSDK_CONTAINER_CLUSTER=${_CLUSTER}'
options:
logging: CLOUD_LOGGING_ONLY

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1 @@
node_modules/**

View File

@ -0,0 +1,25 @@
# Copyright 2019 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.
FROM node:18-alpine
WORKDIR /app
COPY ["package.json", "package-lock.json*", "./"]
RUN npm install
COPY . .
CMD [ "node", "index.js" ]

View File

@ -0,0 +1,27 @@
# Storage API
This application it is a RESTful API that let's you manage the Google Cloud Storage buckets available is a project. In order to do so the application needs to authenticate with a service account that has been granted the Storage Admin (`roles/storage.admin`) role.
Find below the operations that can be performed using it:
* Get buckets in project
curl -v http://localhost:3000/buckets
* Get files in bucket
curl -v http://localhost:3000/buckets/BUCKET_NAME
* Create a bucket
curl -v http://localhost:3000/buckets \
-H'Content-Type: application/json' \
-d @- <<EOF
{
"name": "BUCKET_NAME"
}
EOF
* Delete bucket
curl -v -X DELETE http://localhost:3000/buckets/BUCKET_NAME

View File

@ -0,0 +1,40 @@
# Copyright 2022 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
#
# https://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.
steps:
- id: 'Build image and push it to artifact registry'
name: 'gcr.io/kaniko-project/executor:latest'
args:
- '--destination=${_IMAGE}:${COMMIT_SHA}'
- '--cache=true'
- '--cache-ttl=6h'
- id: 'Create and sign attestation'
name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:latest'
entrypoint: 'bash'
args:
- '-eEuo'
- 'pipefail'
- '-c'
- |
set -x
DIGEST=$(gcloud container images describe ${_IMAGE}:${COMMIT_SHA} \
--format 'value(image_summary.digest)' \
--project ${PROJECT_ID})
gcloud beta container binauthz attestations sign-and-create \
--project="${PROJECT_ID}" \
--artifact-url="${_IMAGE}@$${DIGEST}" \
--attestor="${_ATTESTOR}" \
--keyversion="${_KEY_VERSION}"
options:
logging: CLOUD_LOGGING_ONLY

View File

@ -0,0 +1,81 @@
/**
* Copyright 2022 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.
*/
const express = require('express');
const app = express();
const { Storage } = require('@google-cloud/storage');
const PORT = 3000;
const PROJECT_ID = process.env.PROJECT_ID;
const storage = new Storage();
app.use(express.json());
app.set('json spaces', 2)
app.get('/buckets', async (req, res) => {
try {
const [buckets] = await storage.getBuckets();
res.json(buckets.map(bucket => bucket.name));
} catch (error) {
res.status(500).json({
message: `An error occurred trying to fetch the buckets in project: ${error}`
});
}
});
app.get('/buckets/:name', async (req, res) => {
const name = req.params.name;
try {
const [files] = await storage.bucket(name).getFiles();
res.json(files.map(file => file.name));
} catch (error) {
res.status(500).json({
message: `An error occurred fetch the files in ${name} bucket: ${error}`
});
}
});
app.post('/buckets', async (req, res) => {
const name = req.body.name;
try {
const [bucket] = await storage.createBucket(name);
res.status(201).json({
"name": bucket.name
});
} catch (error) {
res.status(500).json({
message: `An error occurred trying to create ${name} bucket: ${error}`
});
}
});
app.delete('/buckets/:name', async (req, res) => {
const name = req.params.name;
try {
await storage.bucket(name).delete();
res.send()
} catch (error) {
res.status(500).json({
message: `An error occurred trying to delete ${name} bucket: ${error}`
});
}
});
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`)
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/storage": "^5.18.3",
"express": "^4.17.3"
}
}

View File

@ -0,0 +1,274 @@
/**
* Copyright 2022 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.
*/
locals {
prefix = (var.prefix == null || var.prefix == "") ? "" : "${var.prefix}-"
k8s_ns = "apis"
k8s_sa = "storage-api-sa"
image = (
"${var.region}-docker.pkg.dev/${module.project.project_id}/${module.docker_artifact_registry.name}/storage-api"
)
}
module "project" {
source = "../../../modules/project"
billing_account = (var.project_create != null
? var.project_create.billing_account_id
: null
)
parent = (var.project_create != null
? var.project_create.parent
: null
)
prefix = var.project_create == null ? null : var.prefix
name = var.project_id
services = [
"artifactregistry.googleapis.com",
"binaryauthorization.googleapis.com",
"cloudbuild.googleapis.com",
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"container.googleapis.com",
"containeranalysis.googleapis.com",
"sourcerepo.googleapis.com"
]
iam = {
"roles/storage.admin" = [module.sa.iam_email]
"roles/logging.logWriter" = [
module.image_cb_sa.iam_email,
module.app_cb_sa.iam_email
]
"roles/container.viewer" = [module.app_cb_sa.iam_email]
"roles/containeranalysis.occurrences.editor" = [module.image_cb_sa.iam_email]
"roles/containeranalysis.notes.attacher" = [module.image_cb_sa.iam_email]
}
}
module "vpc" {
source = "../../../modules/net-vpc"
project_id = module.project.project_id
name = "${local.prefix}vpc"
subnets = [
{
ip_cidr_range = var.subnet_cidr_block
name = "subnet"
region = var.region
secondary_ip_range = {
pods = var.pods_cidr_block
services = var.services_cidr_block
}
}
]
}
module "nat" {
source = "../../../modules/net-cloudnat"
project_id = module.project.project_id
region = var.region
name = "${local.prefix}nat"
router_network = module.vpc.name
}
module "cluster" {
source = "../../../modules/gke-cluster"
project_id = module.project.project_id
name = "${local.prefix}cluster"
location = var.zone
network = module.vpc.self_link
subnetwork = module.vpc.subnet_self_links["${var.region}/subnet"]
secondary_range_pods = "pods"
secondary_range_services = "services"
private_cluster_config = {
enable_private_nodes = true
enable_private_endpoint = false
master_ipv4_cidr_block = var.master_cidr_block
master_global_access = false
}
enable_binary_authorization = true
workload_identity = true
}
module "cluster_nodepool" {
source = "../../../modules/gke-nodepool"
project_id = module.project.project_id
cluster_name = module.cluster.name
location = var.zone
name = "nodepool"
node_service_account_create = true
initial_node_count = 3
}
module "kms" {
source = "../../../modules/kms"
project_id = module.project.project_id
keyring = { location = var.region, name = "test-keyring" }
keyring_create = true
keys = { test-key = null }
key_purpose = {
test-key = {
purpose = "ASYMMETRIC_SIGN"
version_template = {
algorithm = "RSA_SIGN_PKCS1_4096_SHA512"
protection_level = null
}
}
}
key_iam = {
test-key = {
"roles/cloudkms.publicKeyViewer" = [module.image_cb_sa.iam_email]
"roles/cloudkms.signer" = [module.image_cb_sa.iam_email]
}
}
}
data "google_kms_crypto_key_version" "version" {
crypto_key = module.kms.key_ids["test-key"]
}
module "binauthz" {
source = "../../../modules/binauthz"
project_id = module.project.project_id
default_admission_rule = {
evaluation_mode = "ALWAYS_DENY"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = null
}
cluster_admission_rules = {
"${var.zone}.${module.cluster.name}" = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = ["test-attestor"]
}
}
attestors_config = {
"test-attestor" : {
note_reference = null
pgp_public_keys = null
pkix_public_keys = [{
id = data.google_kms_crypto_key_version.version.id
public_key_pem = data.google_kms_crypto_key_version.version.public_key[0].pem
signature_algorithm = data.google_kms_crypto_key_version.version.public_key[0].algorithm
}]
iam = {
"roles/binaryauthorization.attestorsViewer" = [module.image_cb_sa.iam_email]
}
}
}
}
module "docker_artifact_registry" {
source = "../../../modules/artifact-registry"
project_id = module.project.project_id
location = var.region
format = "DOCKER"
id = "${local.prefix}registry"
iam = {
"roles/artifactregistry.writer" = [module.image_cb_sa.iam_email]
"roles/artifactregistry.reader" = [module.cluster_nodepool.service_account_iam_email]
}
}
module "image_cb_sa" {
source = "../../../modules/iam-service-account"
project_id = module.project.project_id
name = "sa-cb-image"
}
module "image_repo" {
source = "../../../modules/source-repository"
project_id = module.project.project_id
name = "${local.prefix}image"
triggers = {
image-trigger = {
filename = "cloudbuild.yaml"
included_files = null
service_account = module.image_cb_sa.id
template = {
branch_name = "main"
project_id = module.project.project_id
tag_name = null
}
substitutions = {
_IMAGE = local.image
_ATTESTOR = module.binauthz.attestors["test-attestor"].id
_KEY_VERSION = data.google_kms_crypto_key_version.version.name
}
}
}
iam = {
"roles/source.reader" = [module.image_cb_sa.iam_email]
}
}
module "app_cb_sa" {
source = "../../../modules/iam-service-account"
project_id = module.project.project_id
name = "sa-cb-app"
}
module "app_repo" {
source = "../../../modules/source-repository"
project_id = module.project.project_id
name = "${local.prefix}app"
triggers = {
app-trigger = {
filename = "cloudbuild.yaml"
included_files = null
service_account = module.app_cb_sa.id
template = {
branch_name = "main"
project_id = module.project.project_id
tag_name = null
}
substitutions = {
_ZONE = var.zone
_CLUSTER = module.cluster.name
}
}
}
iam = {
"roles/source.reader" = [module.app_cb_sa.iam_email]
}
}
module "sa" {
source = "../../../modules/iam-service-account"
project_id = module.project.project_id
name = "sa-storage-api"
iam = {
"roles/iam.workloadIdentityUser" : ["serviceAccount:${module.cluster.cluster.project}.svc.id.goog[${local.k8s_ns}/${local.k8s_sa}]"]
}
}
resource "local_file" "app_file" {
content = templatefile("${path.module}/templates/app.yaml.tpl", {
k8s_ns = local.k8s_ns
k8s_sa = local.k8s_sa
google_sa = module.sa.email
image = local.image
})
filename = "${path.module}/app/app.yaml"
file_permission = "0666"
}
resource "local_file" "rbac_file" {
content = templatefile("${path.module}/templates/tenant-setup.yaml.tpl", {
k8s_ns = local.k8s_ns
google_sa = module.app_cb_sa.email
})
filename = "${path.module}/tenant-setup.yaml"
file_permission = "0666"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright 2022 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.
*/
output "image_repo_url" {
description = "Image source repository url."
value = "ssh://<USER>@source.developers.google.com:2022/p/${module.project.project_id}/r/${module.image_repo.name}"
}
output "app_repo_url" {
description = "App source repository url."
value = "ssh://<USER>@source.developers.google.com:2022/p/${module.project.project_id}/r/${module.app_repo.name}"
}

View File

@ -0,0 +1,45 @@
# Copyright 2022 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
#
# https://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.
apiVersion: v1
kind: ServiceAccount
metadata:
name: storage-api-sa
namespace: ${k8s_ns}
annotations:
iam.gke.io/gcp-service-account: ${google_sa}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: storage-api-deployment
namespace: ${k8s_ns}
spec:
selector:
matchLabels:
app: storage-api
replicas: 2
template:
metadata:
labels:
app: storage-api
spec:
serviceAccountName: ${k8s_sa}
containers:
- name: storage-api
image: ${image}:DIGEST
ports:
- containerPort: 3000
nodeSelector:
iam.gke.io/gke-metadata-server-enabled: "true"

View File

@ -0,0 +1,54 @@
# Copyright 2022 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
#
# https://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.
apiVersion: v1
kind: Namespace
metadata:
name: ${k8s_ns}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-deployment-manager
namespace: ${k8s_ns}
rules:
- apiGroups:
- ''
- 'extensions'
- 'apps'
resources:
- 'namespaces'
- 'serviceaccounts'
- 'deployments'
verbs:
- 'get'
- 'list'
- 'watch'
- 'create'
- 'update'
- 'patch'
- 'delete'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-deployment-manager
namespace: ${k8s_ns}
subjects:
- kind: User
name: ${google_sa}
roleRef:
kind: Role
name: app-deployment-manager
apiGroup: rbac.authorization.k8s.io

View File

@ -0,0 +1,71 @@
/**
* Copyright 2022 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.
*/
variable "project_create" {
description = "Parameters for the creation of the new project."
type = object({
billing_account_id = string
parent = string
})
default = null
}
variable "project_id" {
description = "Project ID."
type = string
}
variable "prefix" {
description = "Prefix for resources created."
type = string
default = null
}
variable "pods_cidr_block" {
description = "Pods CIDR block."
type = string
default = "172.16.0.0/20"
}
variable "services_cidr_block" {
description = "Services CIDR block."
type = string
default = "192.168.0.0/24"
}
variable "master_cidr_block" {
description = "Master CIDR block."
type = string
default = "10.0.0.0/28"
}
variable "subnet_cidr_block" {
description = "Subnet CIDR block."
type = string
default = "10.0.1.0/24"
}
variable "region" {
description = "Region."
type = string
default = "europe-west1"
}
variable "zone" {
description = "Zone."
type = string
default = "europe-west1-c"
}

View File

@ -0,0 +1,79 @@
# Google Cloud Artifact Registry Module
This module simplifies the creation of a Binary Authorization policy, attestors and attestor IAM bindings.
## Example
### Binary Athorization
```hcl
module "binauthz" {
source = "./modules/binauthz"
project_id = "my_project"
global_policy_evaluation_mode = "DISABLE"
default_admission_rule = {
evaluation_mode = "ALWAYS_DENY"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = null
}
cluster_admission_rules = {
"europe-west1-c.cluster" = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = [ "test" ]
}
}
attestors_config = {
"test": {
note_reference = null
pgp_public_keys = [
<<EOT
mQENBFtP0doBCADF+joTiXWKVuP8kJt3fgpBSjT9h8ezMfKA4aXZctYLx5wslWQl
bB7Iu2ezkECNzoEeU7WxUe8a61pMCh9cisS9H5mB2K2uM4Jnf8tgFeXn3akJDVo0
oR1IC+Dp9mXbRSK3MAvKkOwWlG99sx3uEdvmeBRHBOO+grchLx24EThXFOyP9Fk6
V39j6xMjw4aggLD15B4V0v9JqBDdJiIYFzszZDL6pJwZrzcP0z8JO4rTZd+f64bD
Mpj52j/pQfA8lZHOaAgb1OrthLdMrBAjoDjArV4Ek7vSbrcgYWcI6BhsQrFoxKdX
83TZKai55ZCfCLIskwUIzA1NLVwyzCS+fSN/ABEBAAG0KCJUZXN0IEF0dGVzdG9y
IiA8ZGFuYWhvZmZtYW5AZ29vZ2xlLmNvbT6JAU4EEwEIADgWIQRfWkqHt6hpTA1L
uY060eeM4dc66AUCW0/R2gIbLwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA6
0eeM4dc66HdpCAC4ot3b0OyxPb0Ip+WT2U0PbpTBPJklesuwpIrM4Lh0N+1nVRLC
51WSmVbM8BiAFhLbN9LpdHhds1kUrHF7+wWAjdR8sqAj9otc6HGRM/3qfa2qgh+U
WTEk/3us/rYSi7T7TkMuutRMIa1IkR13uKiW56csEMnbOQpn9rDqwIr5R8nlZP5h
MAU9vdm1DIv567meMqTaVZgR3w7bck2P49AO8lO5ERFpVkErtu/98y+rUy9d789l
+OPuS1NGnxI1YKsNaWJF4uJVuvQuZ1twrhCbGNtVorO2U12+cEq+YtUxj7kmdOC1
qoIRW6y0+UlAc+MbqfL0ziHDOAmcqz1GnROg
=6Bvm
EOT
]
pkix_public_keys = null
iam = {
"roles/viewer" = ["user:user1@my_org.com"]
}
}
}
}
# tftest modules=1 resources=4
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L17) | Project ID. | <code>string</code> | ✓ | |
| [admission_whitelist_patterns](variables.tf#L28) | An image name pattern to allowlist | <code>list&#40;string&#41;</code> | | <code>null</code> |
| [attestors_config](variables.tf#L58) | Attestors configuration | <code title="map&#40;object&#40;&#123;&#10; note_reference &#61; string&#10; iam &#61; map&#40;list&#40;string&#41;&#41;&#10; pgp_public_keys &#61; list&#40;string&#41;&#10; pkix_public_keys &#61; list&#40;object&#40;&#123;&#10; id &#61; string&#10; public_key_pem &#61; string&#10; signature_algorithm &#61; string&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>null</code> |
| [cluster_admission_rules](variables.tf#L48) | Admission rules | <code title="map&#40;object&#40;&#123;&#10; evaluation_mode &#61; string&#10; enforcement_mode &#61; string&#10; attestors &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>null</code> |
| [default_admission_rule](variables.tf#L34) | Default admission rule | <code title="object&#40;&#123;&#10; evaluation_mode &#61; string&#10; enforcement_mode &#61; string&#10; attestors &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; evaluation_mode &#61; &#34;ALWAYS_ALLOW&#34;&#10; enforcement_mode &#61; &#34;ENFORCED_BLOCK_AND_AUDIT_LOG&#34;&#10; attestors &#61; null&#10;&#125;">&#123;&#8230;&#125;</code> |
| [global_policy_evaluation_mode](variables.tf#L22) | Global policy evaluation mode. | <code>string</code> | | <code>null</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [attestors](outputs.tf#L22) | Attestors. | |
| [id](outputs.tf#L17) | Binary Authorization policy ID | |
| [notes](outputs.tf#L30) | Notes. | |
<!-- END TFDOC -->

91
modules/binauthz/main.tf Normal file
View File

@ -0,0 +1,91 @@
/**
* Copyright 2022 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.
*/
resource "google_binary_authorization_policy" "policy" {
project = var.project_id
dynamic "admission_whitelist_patterns" {
for_each = toset(coalesce(var.admission_whitelist_patterns, []))
content {
name_pattern = admission_whitelist_patterns.value
}
}
default_admission_rule {
evaluation_mode = var.default_admission_rule.evaluation_mode
enforcement_mode = var.default_admission_rule.enforcement_mode
require_attestations_by = [for attestor in coalesce(var.default_admission_rule.attestors, []) : google_binary_authorization_attestor.attestors[attestor].name]
}
dynamic "cluster_admission_rules" {
for_each = coalesce(var.cluster_admission_rules, {})
content {
cluster = cluster_admission_rules.key
evaluation_mode = cluster_admission_rules.value.evaluation_mode
enforcement_mode = cluster_admission_rules.value.enforcement_mode
require_attestations_by = [for attestor in cluster_admission_rules.value.attestors : google_binary_authorization_attestor.attestors[attestor].name]
}
}
}
resource "google_binary_authorization_attestor" "attestors" {
for_each = coalesce(var.attestors_config, {})
name = each.key
project = var.project_id
attestation_authority_note {
note_reference = each.value.note_reference == null ? google_container_analysis_note.notes[each.key].name : each.value.note_reference
dynamic "public_keys" {
for_each = coalesce(each.value.pgp_public_keys, [])
content {
ascii_armored_pgp_public_key = public_keys.value
}
}
dynamic "public_keys" {
for_each = {
for pkix_public_key in coalesce(each.value.pkix_public_keys, []) :
"${pkix_public_key.public_key_pem}-${pkix_public_key.signature_algorithm}" => pkix_public_key
}
content {
id = public_keys.value.id
pkix_public_key {
public_key_pem = public_keys.value.public_key_pem
signature_algorithm = public_keys.value.signature_algorithm
}
}
}
}
}
resource "google_binary_authorization_attestor_iam_binding" "bindings" {
for_each = merge(flatten([
for name, attestor_config in var.attestors_config : { for role, members in coalesce(attestor_config.iam, {}) : "${name}-${role}" => {
name = name
role = role
members = members
} }])...)
project = google_binary_authorization_attestor.attestors[each.value.name].project
attestor = google_binary_authorization_attestor.attestors[each.value.name].name
role = each.value.role
members = each.value.members
}
resource "google_container_analysis_note" "notes" {
for_each = toset([for name, attestor_config in var.attestors_config : name if attestor_config.note_reference == null])
name = "${each.value}-note"
project = var.project_id
attestation_authority {
hint {
human_readable_name = "Attestor ${each.value} note"
}
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright 2022 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.
*/
output "id" {
description = "Binary Authorization policy ID"
value = google_binary_authorization_policy.policy.id
}
output "attestors" {
description = "Attestors."
value = google_binary_authorization_attestor.attestors
depends_on = [
google_binary_authorization_attestor_iam_binding.bindings
]
}
output "notes" {
description = "Notes."
value = google_container_analysis_note.notes
}

View File

@ -0,0 +1,71 @@
/**
* Copyright 2022 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.
*/
variable "project_id" {
description = "Project ID."
type = string
}
variable "global_policy_evaluation_mode" {
description = "Global policy evaluation mode."
type = string
default = null
}
variable "admission_whitelist_patterns" {
description = "An image name pattern to allowlist"
type = list(string)
default = null
}
variable "default_admission_rule" {
description = "Default admission rule"
type = object({
evaluation_mode = string
enforcement_mode = string
attestors = list(string)
})
default = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = null
}
}
variable "cluster_admission_rules" {
description = "Admission rules"
type = map(object({
evaluation_mode = string
enforcement_mode = string
attestors = list(string)
}))
default = null
}
variable "attestors_config" {
description = "Attestors configuration"
type = map(object({
note_reference = string
iam = map(list(string))
pgp_public_keys = list(string)
pkix_public_keys = list(object({
id = string
public_key_pem = string
signature_algorithm = string
}))
}))
default = null
}

View File

@ -0,0 +1,29 @@
# Copyright 2022 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
#
# https://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.
terraform {
required_version = ">= 1.1.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.17.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.17.0"
}
}
}

View File

@ -0,0 +1,13 @@
# Copyright 2022 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.

View File

@ -0,0 +1,21 @@
/**
* Copyright 2022 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.
*/
module "test" {
source = "../../../../../examples/cloud-operations/binauthz"
project_create = var.project_create
project_id = var.project_id
}

View File

@ -0,0 +1,26 @@
# Copyright 2022 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
#
# https://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.
variable "project_create" {
type = object({
billing_account_id = string
parent = string
})
default = null
}
variable "project_id" {
type = string
default = "my-project"
}

View File

@ -0,0 +1,19 @@
# Copyright 2022 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.
def test_resources(e2e_plan_runner):
"Test that plan works and the numbers of resources is as expected."
modules, resources = e2e_plan_runner()
assert len(modules) == 13
assert len(resources) == 42

View File

@ -0,0 +1,13 @@
# Copyright 2022 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.

View File

@ -0,0 +1,23 @@
/**
* Copyright 2022 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.
*/
module "test" {
source = "../../../../modules/binauthz"
project_id = var.project_id
global_policy_evaluation_mode = var.global_policy_evaluation_mode
default_admission_rule = var.default_admission_rule
attestors_config = var.attestors_config
}

View File

@ -0,0 +1,103 @@
/**
* Copyright 2022 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.
*/
variable "project_id" {
type = string
default = "my_project"
}
variable "global_policy_evaluation_mode" {
type = string
default = null
}
variable "admission_whitelist_patterns" {
type = list(string)
default = [
"gcr.io/google_containers/*"
]
}
variable "default_admission_rule" {
type = object({
evaluation_mode = string
enforcement_mode = string
attestors = list(string)
})
default = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = null
}
}
variable "cluster_admission_rules" {
type = map(object({
evaluation_mode = string
enforcement_mode = string
attestors = list(string)
}))
default = {
"europe-west1-c.cluster" = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
attestors = ["test"]
}
}
}
variable "attestors_config" {
description = "Attestors configuration"
type = map(object({
note_reference = string
iam = map(list(string))
pgp_public_keys = list(string)
pkix_public_keys = list(object({
id = string
public_key_pem = string
signature_algorithm = string
}))
}))
default = {
"test" : {
note_reference = null
pgp_public_keys = [
<<EOT
mQENBFtP0doBCADF+joTiXWKVuP8kJt3fgpBSjT9h8ezMfKA4aXZctYLx5wslWQl
bB7Iu2ezkECNzoEeU7WxUe8a61pMCh9cisS9H5mB2K2uM4Jnf8tgFeXn3akJDVo0
oR1IC+Dp9mXbRSK3MAvKkOwWlG99sx3uEdvmeBRHBOO+grchLx24EThXFOyP9Fk6
V39j6xMjw4aggLD15B4V0v9JqBDdJiIYFzszZDL6pJwZrzcP0z8JO4rTZd+f64bD
Mpj52j/pQfA8lZHOaAgb1OrthLdMrBAjoDjArV4Ek7vSbrcgYWcI6BhsQrFoxKdX
83TZKai55ZCfCLIskwUIzA1NLVwyzCS+fSN/ABEBAAG0KCJUZXN0IEF0dGVzdG9y
IiA8ZGFuYWhvZmZtYW5AZ29vZ2xlLmNvbT6JAU4EEwEIADgWIQRfWkqHt6hpTA1L
uY060eeM4dc66AUCW0/R2gIbLwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA6
0eeM4dc66HdpCAC4ot3b0OyxPb0Ip+WT2U0PbpTBPJklesuwpIrM4Lh0N+1nVRLC
51WSmVbM8BiAFhLbN9LpdHhds1kUrHF7+wWAjdR8sqAj9otc6HGRM/3qfa2qgh+U
WTEk/3us/rYSi7T7TkMuutRMIa1IkR13uKiW56csEMnbOQpn9rDqwIr5R8nlZP5h
MAU9vdm1DIv567meMqTaVZgR3w7bck2P49AO8lO5ERFpVkErtu/98y+rUy9d789l
+OPuS1NGnxI1YKsNaWJF4uJVuvQuZ1twrhCbGNtVorO2U12+cEq+YtUxj7kmdOC1
qoIRW6y0+UlAc+MbqfL0ziHDOAmcqz1GnROg
=6Bvm
EOT
]
pkix_public_keys = null
iam = {
"roles/viewer" = ["user:user1@my_org.com"]
}
}
}
}

View File

@ -0,0 +1,24 @@
# Copyright 2022 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.
import pytest
@pytest.fixture
def resources(plan_runner):
_, resources = plan_runner()
return resources
def test_resource_count(resources):
"Test number of resources created."
assert len(resources) == 4