├── docs ├── sbom.md ├── bonus.md ├── setup.md └── workshop.md ├── migrations ├── .keep ├── 2022-12-21-034433_create_vote_db │ ├── down.sql │ └── up.sql └── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql ├── .dockerignore ├── imgs ├── build.png ├── deploy.png └── secure-supply-chain-on-aks-overview.png ├── src ├── schema.rs ├── model.rs ├── database.rs └── main.rs ├── diesel.toml ├── terraform ├── terraform.tfvars ├── output.tf ├── variables.tf └── main.tf ├── .gitignore ├── manifests ├── constraint.yaml ├── service-app.yaml ├── service-db.yaml ├── ingress.yaml ├── deployment-db.yaml ├── deployment-app.yaml └── template.yaml ├── scripts ├── duration.sh ├── kustomization.yaml ├── copa-automation.sh ├── build-sign-patch-push.sh ├── install-dev-tools.sh └── git-ops.md ├── Cargo.toml ├── reset.sh ├── README.md ├── static ├── index.html └── css │ └── default.css ├── Dockerfile ├── .devcontainer └── devcontainer.json ├── bonus.sh ├── setup.sh ├── util.sh ├── .github └── workflows │ ├── main.yml │ └── patch.yml ├── demo.sh └── Cargo.lock /docs/sbom.md: -------------------------------------------------------------------------------- 1 | TODO -------------------------------------------------------------------------------- /migrations/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | charts/ 3 | target 4 | -------------------------------------------------------------------------------- /migrations/2022-12-21-034433_create_vote_db/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE votes -------------------------------------------------------------------------------- /imgs/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duffney/secure-supply-chain-on-aks/HEAD/imgs/build.png -------------------------------------------------------------------------------- /imgs/deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duffney/secure-supply-chain-on-aks/HEAD/imgs/deploy.png -------------------------------------------------------------------------------- /migrations/2022-12-21-034433_create_vote_db/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE votes ( 2 | vote_id SERIAL PRIMARY KEY, 3 | vote_value VARCHAR(4) NOT NULL 4 | ) -------------------------------------------------------------------------------- /imgs/secure-supply-chain-on-aks-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duffney/secure-supply-chain-on-aks/HEAD/imgs/secure-supply-chain-on-aks-overview.png -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | diesel::table! { 4 | votes (vote_id) { 5 | vote_id -> Int4, 6 | vote_value -> Varchar, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | 7 | [migrations_directory] 8 | dir = "migrations" 9 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | registry_name = "s3cexampleacr" 2 | key_vault_name = "s3cexample-kv" 3 | identity_name = "ratify-workload-identity" 4 | resource_group_name = "s3cexample-rg" 5 | cluster_name = "s3cexample-aks" 6 | tags = { 7 | environment = "dev" 8 | department = "example" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore .terraform directories 2 | **/.terraform/* 3 | **/*.tfstate 4 | **/*.tfstate.* 5 | **/README.md 6 | **/terraform/.terraform.lock.hcl 7 | /target 8 | .env 9 | .DS_Store 10 | copa 11 | trivy 12 | notation 13 | bin/* 14 | contrib/* 15 | patch.json 16 | demo.env 17 | buildkitd 18 | LICENSE -------------------------------------------------------------------------------- /manifests/constraint.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: constraints.gatekeeper.sh/v1beta1 2 | kind: RatifyVerificationDeployment 3 | metadata: 4 | name: ratify-constraint-deployment 5 | spec: 6 | enforcementAction: deny 7 | match: 8 | kinds: 9 | - apiGroups: ["*"] 10 | kinds: ["Deployment"] 11 | namespaces: ["default"] -------------------------------------------------------------------------------- /manifests/service-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: azure-voting-app 7 | name: azure-voting-app 8 | spec: 9 | ports: 10 | - port: 80 11 | protocol: TCP 12 | targetPort: 8080 13 | selector: 14 | app: azure-voting-app 15 | type: ClusterIP 16 | status: 17 | loadBalancer: {} -------------------------------------------------------------------------------- /manifests/service-db.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: azure-voting-db 7 | name: azure-voting-db 8 | spec: 9 | ports: 10 | - port: 5432 11 | protocol: TCP 12 | targetPort: 5432 13 | selector: 14 | app: azure-voting-db 15 | type: ClusterIP 16 | status: 17 | loadBalancer: {} 18 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use super::schema::votes; 2 | use diesel::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Deserialize, Queryable, Serialize)] 6 | pub struct Vote { 7 | pub vote_id: i32, 8 | pub vote_value: String, 9 | } 10 | 11 | #[derive(Deserialize, Insertable, Serialize)] 12 | #[diesel(table_name=votes)] 13 | pub struct NewVote { 14 | pub vote_value: String, 15 | } 16 | -------------------------------------------------------------------------------- /manifests/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | creationTimestamp: null 5 | name: azure-voting-app 6 | spec: 7 | ingressClassName: webapprouting.kubernetes.azure.com 8 | rules: 9 | - http: 10 | paths: 11 | - backend: 12 | service: 13 | name: azure-voting-app 14 | port: 15 | number: 80 16 | path: / 17 | pathType: Prefix 18 | status: 19 | loadBalancer: {} 20 | -------------------------------------------------------------------------------- /scripts/duration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | start_time=$(date +%s) 4 | 5 | # Your command here 6 | # ./trivy image brk264hacr.azurecr.io/azure-voting-app-rust:v0.1-alpha --scanners vuln 7 | image=brk264hacr.azurecr.io/azure-voting-app-rust:v0.1-alpha 8 | # ./trivy image $image --scanners vuln --vuln-type os --severity HIGH,CRITICAL 9 | ./trivy image --exit-code 0 --format json --output patch.json --scanners vuln --vuln-type os --ignore-unfixed $image 10 | 11 | end_time=$(date +%s) 12 | duration=$((end_time - start_time)) 13 | 14 | echo "Command duration: $duration seconds" 15 | -------------------------------------------------------------------------------- /scripts/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: default 4 | resources: 5 | - deployment-app.yaml 6 | - deployment-db.yaml 7 | - ingress.yaml 8 | - service-app.yaml 9 | - service-db.yaml 10 | images: 11 | - name: azure-voting-app-rust 12 | newName: s3cexampleacr.azurecr.io/azure-voting-app-rust # {"$imagepolicy": "flux-system:azure-voting:name"} 13 | newTag: v0.1.0-alpha-2 # {"$imagepolicy": "flux-system:azure-voting:tag"} 14 | - name: postgres 15 | newName: s3cexampleacr.azurecr.io/postgres 16 | newTag: 15.0-alpine 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "azure-voting-app-rust" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | actix-files = "0.6.2" 10 | actix-web = "4.2.1" 11 | diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } 12 | diesel_migrations = "2.0.0" 13 | dotenvy = "0.15.6" 14 | env_logger = "0.10.0" 15 | handlebars = { version = "4.3.6", features = ["dir_source"] } 16 | lazy_static = "1.4.0" 17 | log = "0.4.17" 18 | r2d2 = "0.8.10" 19 | serde = { version = "1.0.151", features = ["derive"] } 20 | serde_json = "1.0.91" 21 | -------------------------------------------------------------------------------- /manifests/deployment-db.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: azure-voting-db 7 | name: azure-voting-db 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: azure-voting-db 13 | strategy: {} 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | app: azure-voting-db 19 | spec: 20 | containers: 21 | - image: postgres:15.0-alpine 22 | name: postgres 23 | ports: 24 | - containerPort: 5432 25 | env: 26 | - name: POSTGRES_PASSWORD 27 | value: "mypassword" 28 | resources: {} 29 | status: {} 30 | -------------------------------------------------------------------------------- /terraform/output.tf: -------------------------------------------------------------------------------- 1 | output "rg_name" { 2 | value = azurerm_resource_group.rg.name 3 | } 4 | 5 | output "aks_name" { 6 | value = azurerm_kubernetes_cluster.aks.name 7 | } 8 | 9 | output "akv_uri" { 10 | value = azurerm_key_vault.kv.vault_uri 11 | } 12 | 13 | output "akv_name" { 14 | value = azurerm_key_vault.kv.name 15 | } 16 | 17 | output "acr_name" { 18 | value = azurerm_container_registry.registry.name 19 | } 20 | 21 | output "cert_name" { 22 | value = azurerm_key_vault_certificate.sign-cert.name 23 | } 24 | 25 | output "tenant_id" { 26 | value = data.azurerm_client_config.current.tenant_id 27 | } 28 | 29 | output "wl_client_id" { 30 | value = azurerm_user_assigned_identity.identity.client_id 31 | } -------------------------------------------------------------------------------- /reset.sh: -------------------------------------------------------------------------------- 1 | cd terraform/ 2 | export ACR_NAME="$(terraform output -raw acr_name)" 3 | ACR_IMAGE=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha 4 | terraform destroy --auto-approve 5 | cd .. 6 | 7 | ACR_IMAGE_PATCHED=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha-1 8 | APP_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $ACR_IMAGE_PATCHED) 9 | sed -i "s|${APP_DIGEST}|azure-voting-app-rust:v0.1-alpha|" ./manifests/deployment-app.yaml 10 | 11 | DB_DIGEST=$(docker image inspect --format='{{range $digest := .RepoDigests}}{{println $digest}}{{end}}' ${ACR_NAME}.azurecr.io/postgres:15.0-alpine | sort | tail -1) 12 | sed -i "s|${DB_DIGEST}|postgres:15.0-alpine|" ./manifests/deployment-db.yaml 13 | 14 | docker rmi -f $(docker images -aq) -------------------------------------------------------------------------------- /manifests/deployment-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: azure-voting-app 7 | name: azure-voting-app 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: azure-voting-app 13 | strategy: {} 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | app: azure-voting-app 19 | spec: 20 | containers: 21 | - image: azure-voting-app-rust:v0.1-alpha 22 | name: azure-voting-app-rust 23 | ports: 24 | - containerPort: 8080 25 | env: 26 | - name: DATABASE_SERVER 27 | value: azure-voting-db 28 | - name: DATABASE_PASSWORD 29 | value: mypassword 30 | resources: {} 31 | status: {} 32 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "registry_name" { 2 | type = string 3 | default = "myregistry" 4 | } 5 | 6 | variable "key_vault_name" { 7 | type = string 8 | default = "mykeyvault" 9 | } 10 | 11 | variable "resource_group_name" { 12 | type = string 13 | default = "myresourcegroup" 14 | } 15 | 16 | variable "location" { 17 | type = string 18 | default = "eastus" 19 | } 20 | 21 | variable "identity_name" { 22 | type = string 23 | default = "myidentity" 24 | } 25 | 26 | variable "cluster_name" { 27 | type = string 28 | default = "mycluster" 29 | } 30 | 31 | variable "tags" { 32 | type = map(string) 33 | } 34 | 35 | variable "ratify_namespace" { 36 | type = string 37 | default = "gatekeeper-system" 38 | } 39 | 40 | variable "sign_cert_name" { 41 | type = string 42 | default = "default" 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔑 Secure Supply Chain on AKS 2 | 3 | ![Secure Supply Chain on AKS Overview](imgs/secure-supply-chain-on-aks-overview.png) 4 | 5 | Learn how to secure your container deployments on Azure Kubernetes Service by using open-source tools. 6 | 7 | - [Trivy](https://github.com/aquasecurity/trivy) scans container images for vulnerabilities 8 | - [Coptaic](https://github.com/project-copacetic/copacetic) patches container images using reports from vulnerability scanners 9 | - [Notary](https://github.com/notaryproject/notary) signs container images with digital signatures 10 | - [Gatekeeper](https://github.com/open-policy-agent/gatekeeper-library) policy enforcement for Kubernetes 11 | - [Ratify](https://github.com/deislabs/ratify) admission controller for Kubernetes 12 | 13 | By combining these tools, you can create a secure supply chain for your container deployments on Azure Kubernetes Service. 14 | 15 | 👉 [Watch the MSBuild 2023 Talk](https://www.youtube.com/watch?v=Mep9QWc3ByE&t=1s) 16 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{title}} 7 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 |
{{button1}} - {{ value1 }} | {{button2}} - {{ value2 }}
25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################# 2 | ## build stage ## 3 | ################# 4 | FROM rust:1.68.1-bullseye AS builder 5 | WORKDIR /code 6 | 7 | # Download crates-io index and fetch dependency code. 8 | # This step avoids needing to spend time on every build downloading the index 9 | # which can take a long time within the docker context. Docker will cache it. 10 | RUN USER=root cargo init 11 | COPY Cargo.toml Cargo.toml 12 | COPY Cargo.lock Cargo.lock 13 | RUN cargo fetch 14 | 15 | # copy app files 16 | COPY src src 17 | COPY migrations migrations 18 | COPY static static 19 | 20 | # compile app 21 | RUN cargo build --release 22 | 23 | ############### 24 | ## run stage ## 25 | ############### 26 | FROM debian:bullseye-20211220 27 | # FROM debian:bullseye-20230411 28 | WORKDIR /app 29 | 30 | RUN USER=root apt update -y 31 | RUN USER=root apt install libpq5 -y 32 | 33 | # copy server binary from build stage 34 | COPY --from=builder /code/target/release/azure-voting-app-rust azure-voting-app-rust 35 | COPY static static 36 | 37 | # set user to non-root unless root is required for your app 38 | USER 1001 39 | 40 | # indicate what port the server is running on 41 | ENV PORT 8080 42 | EXPOSE 8080 43 | 44 | # run server 45 | CMD [ "/app/azure-voting-app-rust" ] 46 | -------------------------------------------------------------------------------- /scripts/copa-automation.sh: -------------------------------------------------------------------------------- 1 | flux create secret git aks-store-demo \ 2 | --url=$GITHUB_REPO_URL \ 3 | --username=$GITHUB_USER \ 4 | --password=$GITHUB_TOKEN 5 | 6 | 7 | image repository 8 | 9 | flux create image repository store-front \ 10 | --image=s3cexampleacr.azurecr.io/azure-voting-app-rust \ 11 | --interval=1m \ 12 | --export > ./clusters/dev/aks-store-demo-store-front-image.yaml 13 | 14 | image policy 15 | 16 | flux create image policy store-front \ 17 | --image-ref=store-front \ 18 | --select-semver='>=1.0.0-0' \ # update this to get the latest patched version 19 | --export > ./clusters/dev/aks-store-demo-store-front-image-policy.yaml 20 | 21 | image update automation 22 | 23 | flux create image update store-front \ 24 | --interval=1m \ 25 | --git-repo-ref=azure-voting \ 26 | --git-repo-path="./manifests" \ 27 | --checkout-branch=main \ 28 | --author-name=fluxcdbot \ 29 | --author-email=fluxcdbot@users.noreply.github.com \ 30 | --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" \ 31 | --export > ./clusters/dev/azure-voting-image-update.yaml 32 | 33 | Mark the manifests 34 | 35 | image: # {"$imagepolicy": "flux-system:store-front"} 36 | 37 | https://github.com/pauldotyu/aks-store-demo-manifests/blob/istio/overlays/dev/kustomization.yaml -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /scripts/build-sign-patch-push.sh: -------------------------------------------------------------------------------- 1 | cd terraform/; 2 | ACR_NAME="$(terraform output -raw acr_name)" 3 | CERT_NAME="$(terraform output -raw cert_name)" 4 | KEYVAULT_NAME="$(terraform output -raw akv_name)" 5 | cd .. 6 | 7 | ACR_IMAGE=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha 8 | docker build -t $ACR_IMAGE . 9 | docker push $ACR_IMAGE 10 | 11 | 12 | trivy image --exit-code 0 --format json --output ./patch.json --scanners vuln --vuln-type os --ignore-unfixed $ACR_IMAGE 13 | sudo ./bin/buildkitd &> /dev/null & 14 | sudo copa patch -i ${ACR_IMAGE} -r ./patch.json -t v0.1-alpha-1 15 | ACR_IMAGE_PATCHED=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha-1 16 | docker push $ACR_IMAGE_PATCHED 17 | 18 | KEY_ID=$(az keyvault certificate show --name $CERT_NAME --vault-name $KEYVAULT_NAME --query kid -o tsv) 19 | notation key add --plugin azure-kv $CERT_NAME --id $KEY_ID --default 20 | APP_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $ACR_IMAGE_PATCHED) 21 | notation sign $APP_DIGEST 22 | 23 | docker pull postgres:15.0-alpine 24 | docker tag postgres:15.0-alpine $ACR_NAME.azurecr.io/postgres:15.0-alpine 25 | docker push ${ACR_NAME}.azurecr.io/postgres:15.0-alpine 26 | 27 | DB_DIGEST=$(docker image inspect --format='{{range $digest := .RepoDigests}}{{println $digest}}{{end}}' ${ACR_NAME}.azurecr.io/postgres:15.0-alpine | sort | tail -1) 28 | notation sign $DB_DIGEST -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "uname -a", 16 | "postCreateCommand": "bash scripts/install-dev-tools.sh", 17 | "features": { 18 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 19 | "ghcr.io/devcontainers/features/azure-cli:1": {}, 20 | "ghcr.io/devcontainers/features/terraform:1": {}, 21 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {} 22 | }, 23 | 24 | 25 | // Configure tool-specific properties. 26 | "customizations": { 27 | "vscode": { 28 | "extensions": ["GitHub.vscode-github-actions"] 29 | } 30 | } 31 | 32 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 33 | // "remoteUser": "root" 34 | } 35 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use diesel::{pg::PgConnection, prelude::*, r2d2::ConnectionManager}; 2 | use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; 3 | use dotenvy::dotenv; 4 | use log::{error, info}; 5 | use r2d2::Pool; 6 | use std::env; 7 | use std::time::Duration; 8 | 9 | pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); 10 | 11 | pub fn setup() -> Pool> { 12 | dotenv().ok(); 13 | let database_password = env::var("DATABASE_PASSWORD").expect("DATABASE_PASSWORD must be set"); 14 | let database_server = env::var("DATABASE_SERVER").expect("DATABASE_SERVER must be set"); 15 | let database_url = format!("postgres://postgres:{}@{}",database_password, database_server); 16 | 17 | info!("Establishing database connection"); 18 | let mut connection: PgConnection; 19 | loop { 20 | match PgConnection::establish(&database_url) { 21 | Ok(conn) => { 22 | connection = conn; 23 | break; 24 | } 25 | Err(_) => { 26 | error!("Failed to establish a database connection, retrying in 5 seconds"); 27 | std::thread::sleep(Duration::from_secs(5)); 28 | } 29 | } 30 | } 31 | 32 | info!("Apply database migrations"); 33 | let _ = connection.run_pending_migrations(MIGRATIONS); 34 | 35 | info!("Setting up database connection pool"); 36 | let manager = ConnectionManager::::new(&database_url); 37 | r2d2::Pool::builder() 38 | .build(manager) 39 | .expect("Failed to create DB connection pool") 40 | } 41 | -------------------------------------------------------------------------------- /manifests/template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: templates.gatekeeper.sh/v1beta1 2 | kind: ConstraintTemplate 3 | metadata: 4 | name: ratifyverificationdeployment 5 | spec: 6 | crd: 7 | spec: 8 | names: 9 | kind: RatifyVerificationDeployment 10 | targets: 11 | - target: admission.k8s.gatekeeper.sh 12 | rego: | 13 | package ratifyverificationdeployment 14 | 15 | # Get data from Ratify 16 | remote_data := response { 17 | images := [img | img = input.review.object.spec.template.spec.containers[_].image] 18 | response := external_data({"provider": "ratify-provider", "keys": images}) 19 | } 20 | 21 | # Base Gatekeeper violation 22 | violation[{"msg": msg}] { 23 | general_violation[{"result": msg}] 24 | } 25 | 26 | # Check if there are any system errors 27 | general_violation[{"result": result}] { 28 | err := remote_data.system_error 29 | err != "" 30 | result := sprintf("System error calling external data provider: %s", [err]) 31 | } 32 | 33 | # Check if there are errors for any of the images 34 | general_violation[{"result": result}] { 35 | count(remote_data.errors) > 0 36 | result := sprintf("Error validating one or more images: %s", remote_data.errors) 37 | } 38 | 39 | # Check if the success criteria is true 40 | general_violation[{"result": result}] { 41 | subject_validation := remote_data.responses[_] 42 | subject_validation[1].isSuccess == false 43 | result := sprintf("Subject failed verification: %s", [subject_validation[0]]) 44 | } -------------------------------------------------------------------------------- /bonus.sh: -------------------------------------------------------------------------------- 1 | . ./util.sh 2 | 3 | run 'clear' 4 | 5 | # desc 'Sign into gh cli' 6 | run 'gh auth login' 7 | 8 | desc 'Refresh Azure resources variables...' 9 | export SUBSCRIPTION_ID=$(az account show --query id --output tsv); 10 | cd terraform/; 11 | export GROUP_NAME="$(terraform output -raw rg_name)" 12 | export AKV_NAME="$(terraform output -raw akv_name)" 13 | export ACR_NAME="$(terraform output -raw acr_name)" 14 | export CERT_NAME="$(terraform output -raw cert_name)" 15 | cd .. 16 | 17 | desc 'Create an Azure Service Principal' 18 | spDisplayName=github-workflow-sp 19 | run "az ad sp create-for-rbac --name $spDisplayName --role contributor --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/${GROUP_NAME} --sdk-auth" 20 | credJSON=$(az ad sp create-for-rbac --name $spDisplayName --role contributor --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$GROUP_NAME --sdk-auth --only-show-errors) >> /dev/null 2>&1 21 | 22 | desc 'Create an access policy for the signing cert' 23 | objectId=$(az ad sp list --display-name $spDisplayName --query '[].id' --output tsv) 24 | run "az keyvault set-policy --name ${AKV_NAME} --object-id ${objectId} --certificate-permissions get --key-permissions sign --secret-permissions get" 25 | 26 | desc "Create the AZURE_CREDENTIALS secret" 27 | run "gh secret set AZURE_CREDENTIALS --body \"${credJSON}\"" 28 | 29 | desc "Create GitHub Action variables" 30 | KEY_ID=$(az keyvault certificate show --name $CERT_NAME --vault-name $AKV_NAME --query kid -o tsv) 31 | run "gh variable set ACR_NAME --body \"$ACR_NAME\"" 32 | run "gh variable set CERT_NAME --body \"$CERT_NAME\"" 33 | run "gh variable set KEY_ID --body \"$KEY_ID\"" 34 | 35 | desc "Trigger the GitHub Actions workflow" 36 | run "echo >> README.md" 37 | run "git add README.md; git commit -m \"Demo: Let's do it LIVE!\"; git push" 38 | 39 | desc "Check workflow status" 40 | run "echo 'https://aka.ms/secure-supply-chain-on-aks'" -------------------------------------------------------------------------------- /static/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color:#F8F8F8; 3 | } 4 | 5 | div#container { 6 | margin-top:5%; 7 | } 8 | 9 | div#space { 10 | display:block; 11 | margin: 0 auto; 12 | width: 500px; 13 | height: 10px; 14 | 15 | } 16 | 17 | div#logo { 18 | display:block; 19 | margin: 0 auto; 20 | width: 500px; 21 | text-align: center; 22 | font-size:30px; 23 | font-family:Helvetica; 24 | /*border-bottom: 1px solid black;*/ 25 | } 26 | 27 | div#form { 28 | padding: 20px; 29 | padding-right: 20px; 30 | padding-top: 20px; 31 | display:block; 32 | margin: 0 auto; 33 | width: 500px; 34 | text-align: center; 35 | font-size:30px; 36 | font-family:Helvetica; 37 | border-bottom: 1px solid black; 38 | border-top: 1px solid black; 39 | } 40 | 41 | div#results { 42 | display:block; 43 | margin: 0 auto; 44 | width: 500px; 45 | text-align: center; 46 | font-size:30px; 47 | font-family:Helvetica; 48 | } 49 | 50 | .button { 51 | background-color: #4CAF50; /* Green */ 52 | border: none; 53 | color: white; 54 | padding: 16px 32px; 55 | text-align: center; 56 | text-decoration: none; 57 | display: inline-block; 58 | font-size: 16px; 59 | margin: 4px 2px; 60 | -webkit-transition-duration: 0.4s; /* Safari */ 61 | transition-duration: 0.4s; 62 | cursor: pointer; 63 | width: 250px; 64 | } 65 | 66 | .button1 { 67 | background-color: white; 68 | color: black; 69 | border: 2px solid #008CBA; 70 | } 71 | 72 | .button1:hover { 73 | background-color: #008CBA; 74 | color: white; 75 | } 76 | .button2 { 77 | background-color: white; 78 | color: black; 79 | border: 2px solid #555555; 80 | } 81 | 82 | .button2:hover { 83 | background-color: #555555; 84 | color: white; 85 | } 86 | 87 | .button3 { 88 | background-color: white; 89 | color: black; 90 | border: 2px solid #f44336; 91 | } 92 | 93 | .button3:hover { 94 | background-color: #f44336; 95 | color: white; 96 | } -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | # az login > /dev/null 2>&1 2 | 3 | # sudo apt -y install xdg-utils; 4 | cd terraform/; 5 | terraform init && terraform apply --auto-approve; 6 | export GROUP_NAME="$(terraform output -raw rg_name)" 7 | export AKS_NAME="$(terraform output -raw aks_name)" 8 | export VAULT_URI="$(terraform output -raw akv_uri)" 9 | export KEYVAULT_NAME="$(terraform output -raw akv_name)" 10 | export ACR_NAME="$(terraform output -raw acr_name)" 11 | export CERT_NAME="$(terraform output -raw cert_name)" 12 | export TENANT_ID="$(terraform output -raw tenant_id)" 13 | export CLIENT_ID="$(terraform output -raw wl_client_id)" 14 | cd .. 15 | az acr login --name $ACR_NAME >> /dev/null 2>&1; 16 | 17 | az aks get-credentials --resource-group ${GROUP_NAME} --name ${AKS_NAME} 18 | 19 | helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts 20 | 21 | helm install gatekeeper/gatekeeper \ 22 | --name-template=gatekeeper \ 23 | --namespace gatekeeper-system --create-namespace \ 24 | --set enableExternalData=true \ 25 | --set validatingWebhookTimeoutSeconds=5 \ 26 | --set mutatingWebhookTimeoutSeconds=2 27 | 28 | helm repo add ratify https://deislabs.github.io/ratify 29 | 30 | helm install ratify \ 31 | ratify/ratify --atomic \ 32 | --namespace gatekeeper-system \ 33 | --set akvCertConfig.enabled=true \ 34 | --set featureFlags.RATIFY_CERT_ROTATION=true \ 35 | --set akvCertConfig.vaultURI=${VAULT_URI} \ 36 | --set akvCertConfig.cert1Name=${CERT_NAME} \ 37 | --set akvCertConfig.tenantId=${TENANT_ID} \ 38 | --set oras.authProviders.azureWorkloadIdentityEnabled=true \ 39 | --set azureWorkloadIdentity.clientId=${CLIENT_ID} 40 | 41 | # kubectl apply -f https://deislabs.github.io/ratify/library/default/template.yaml 42 | # kubectl apply -f https://deislabs.github.io/ratify/library/default/samples/constraint.yaml 43 | 44 | kubectl apply -f manifests/template.yaml 45 | kubectl apply -f manifests/constraint.yaml 46 | 47 | kubectl get pods --namespace gatekeeper-system -------------------------------------------------------------------------------- /scripts/install-dev-tools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo apt update; 4 | sudo apt install pv; 5 | 6 | curl -L -o trivy_0.41.0_Linux-64bit.tar.gz https://github.com/aquasecurity/trivy/releases/download/v0.41.0/trivy_0.41.0_Linux-64bit.tar.gz; 7 | tar -xzf trivy_0.41.0_Linux-64bit.tar.gz trivy; 8 | rm trivy_0.41.0_Linux-64bit.tar.gz; 9 | sudo mv trivy /bin; 10 | 11 | curl -L -o copa_0.2.0_linux_amd64.tar.gz https://github.com/project-copacetic/copacetic/releases/download/v0.2.0/copa_0.2.0_linux_amd64.tar.gz; 12 | tar -xzf copa_0.2.0_linux_amd64.tar.gz copa; 13 | rm copa_0.2.0_linux_amd64.tar.gz; 14 | sudo mv copa /bin; 15 | 16 | 17 | curl -L -o buildkit-v0.11.6.linux-amd64.tar.gz https://github.com/moby/buildkit/releases/download/v0.11.6/buildkit-v0.11.6.linux-amd64.tar.gz; 18 | tar -xzf buildkit-v0.11.6.linux-amd64.tar.gz; 19 | rm buildkit-v0.11.6.linux-amd64.tar.gz; 20 | 21 | curl -L -o notation_1.1.1_linux_amd64.tar.gz https://github.com/notaryproject/notation/releases/download/v1.1.1/notation_1.1.1_linux_amd64.tar.gz; 22 | tar -xzf notation_1.1.1_linux_amd64.tar.gz; 23 | rm notation_1.1.1_linux_amd64.tar.gz; 24 | sudo mv notation /bin; 25 | 26 | curl -L -o notation-azure-kv_1.2.0_linux_amd64.tar.gz https://github.com/Azure/notation-azure-kv/releases/download/v1.2.0/notation-azure-kv_1.2.0_linux_amd64.tar.gz; 27 | tar -xzf notation-azure-kv_1.2.0_linux_amd64.tar.gz; 28 | rm notation-azure-kv_1.2.0_linux_amd64.tar.gz; 29 | 30 | mkdir -p "${HOME}/.config/notation/plugins/azure-kv"; 31 | mv notation-azure-kv "${HOME}/.config/notation/plugins/azure-kv/" 32 | 33 | type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) 34 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ 35 | && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ 36 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ 37 | && sudo apt update \ 38 | && sudo apt install gh -y 39 | 40 | curl -s https://fluxcd.io/install.sh | sudo bash 41 | 42 | sudo apt-get install gnupg2 -y -------------------------------------------------------------------------------- /util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2016 The Kubernetes Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | readonly reset=$(tput sgr0) 17 | readonly green=$(tput bold; tput setaf 2) 18 | readonly yellow=$(tput bold; tput setaf 3) 19 | readonly blue=$(tput bold; tput setaf 6) 20 | readonly timeout=$(if [ "$(uname)" == "Darwin" ]; then echo "1"; else echo "0.1"; fi) 21 | 22 | function desc() { 23 | maybe_first_prompt 24 | echo -e "$blue# $@$reset" 25 | prompt 26 | } 27 | 28 | function prompt() { 29 | echo -n "$yellow\$ $reset" 30 | } 31 | 32 | started="" 33 | function maybe_first_prompt() { 34 | if [ -z "$started" ]; then 35 | prompt 36 | started=true 37 | fi 38 | } 39 | 40 | # After a `run` this variable will hold the stdout of the command that was run. 41 | # If the command was interactive, this will likely be garbage. 42 | DEMO_RUN_STDOUT="" 43 | 44 | function run() { 45 | maybe_first_prompt 46 | rate=25 47 | if [ -n "$DEMO_RUN_FAST" ]; then 48 | rate=1000 49 | fi 50 | echo "$green$1$reset" | pv -qL $rate 51 | if [ -n "$DEMO_RUN_FAST" ]; then 52 | sleep 0.5 53 | fi 54 | OFILE="$(mktemp -t $(basename $0).XXXXXX)" 55 | script -eq -c "$1" -f "$OFILE" 56 | r=$? 57 | read -d '' -t "${timeout}" -n 10000 # clear stdin 58 | prompt 59 | if [ -z "$DEMO_AUTO_RUN" ]; then 60 | read -s 61 | fi 62 | DEMO_RUN_STDOUT="$(tail -n +2 $OFILE | sed 's/\r//g')" 63 | return $r 64 | } 65 | 66 | function relative() { 67 | for arg; do 68 | echo "$(realpath $(dirname $(which $0)))/$arg" | sed "s|$(realpath $(pwd))|.|" 69 | done 70 | } 71 | 72 | trap "echo" EXIT -------------------------------------------------------------------------------- /scripts/git-ops.md: -------------------------------------------------------------------------------- 1 | prerequisites: 2 | - Create access token on github 3 | - Setup the Azure environment 4 | - Configure the GitHub action workflow 5 | 6 | ## Setup local env 7 | 8 | ```bash 9 | git clone 10 | ``` 11 | 12 | ## Install Flux and Kustomize 13 | 14 | #TODO: Add to install-dev-tools.sh 15 | ```bash 16 | curl -s https://fluxcd.io/install.sh | sudo bash 17 | curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash 18 | ``` 19 | 20 | ## Boostrap FluxCD on your Cluster 21 | 22 | ```bash 23 | export GITHUB_TOKEN= 24 | export GITHUB_USER= 25 | 26 | flux bootstrap github \ 27 | --token-auth \ 28 | --owner=$GITHUB_USER \ 29 | --repository=secure-supply-chain-on-aks \ 30 | --branch=copaGitOps \ 31 | --path=clusters/my-cluster \ 32 | --personal 33 | ``` 34 | 35 | ```bash 36 | git pull 37 | ``` 38 | 39 | ### Create a Source for the azure-voting app 40 | 41 | ```bash 42 | flux create secret git azure-voting \ 43 | --url=https://github.com/duffney/secure-supply-chain-on-aks \ 44 | --username=$GITHUB_USER \ 45 | --password=$GITHUB_TOKEN 46 | ``` 47 | 48 | ```bash 49 | flux create source git azure-voting \ 50 | --url=https://github.com/duffney/secure-supply-chain-on-aks/ \ 51 | --branch=GitOps \ 52 | --interval=1m \ 53 | --secret-ref azure-voting \ 54 | --export > ./clusters/my-cluster/azure-voting-source.yaml 55 | 56 | #TODO --secret-ref 57 | 58 | flux get sources git 59 | ``` 60 | 61 | ### Create a Kustomization for the azure-voting app 62 | 63 | ```bash 64 | 65 | flux create kustomization azure-voting \ 66 | --source=azure-voting \ 67 | --path="./manifests" \ 68 | --prune=true \ 69 | --wait=true \ 70 | --interval=1m \ 71 | --retry-interval=2m \ 72 | --health-check-timeout=3m \ 73 | --export > ./clusters/my-cluster/azure-voting-kustomization.yaml 74 | 75 | flux get kustomizations 76 | ``` 77 | 78 | ```bash 79 | git add ; git commit -m "Add azure-voting app" ; git push 80 | ``` 81 | 82 | 84 | 85 | ### View FluxCD logs & events 86 | 87 | ```bash 88 | flux events 89 | flux logs 90 | ``` 91 | 92 | ### View the azure-voting app 93 | 94 | ```bash 95 | kubectl get pods 96 | kubectl get ingress 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build-Publish-Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | tag: v0.1.0-alpha 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v3 17 | 18 | - name: Login to Azure 19 | uses: azure/login@v1 20 | with: 21 | creds: ${{ secrets.AZURE_CREDENTIALS }} 22 | 23 | - name: Login to Azure Container Registry 24 | run: | 25 | az acr login --name ${{ vars.ACR_NAME }} 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v4.1.1 29 | with: 30 | context: . 31 | push: true 32 | tags: ${{ vars.ACR_NAME }}.azurecr.io/azure-voting-app-rust:${{ env.tag }} 33 | 34 | - name: Aqua Security Trivy Scan 35 | uses: aquasecurity/trivy-action@0.10.0 36 | with: 37 | image-ref: ${{ vars.ACR_NAME }}.azurecr.io/azure-voting-app-rust:${{ env.tag }} 38 | format: json 39 | output: patch.json 40 | vuln-type: os 41 | exit-code: 0 42 | ignore-unfixed: true 43 | severity: CRITICAL 44 | 45 | - name: Patch with Copacetic 46 | run: | 47 | function download() { 48 | DOWNLOAD_URL=$1 49 | DOWNLOAD_FILE=$2 50 | curl -L -o $DOWNLOAD_FILE $DOWNLOAD_URL 51 | tar -xzf $DOWNLOAD_FILE 52 | rm $DOWNLOAD_FILE 53 | } 54 | download https://github.com/project-copacetic/copacetic/releases/download/v0.2.0/copa_0.2.0_linux_amd64.tar.gz copa_0.2.0_linux_amd64.tar.gz 55 | download https://github.com/moby/buildkit/releases/download/v0.11.6/buildkit-v0.11.6.linux-amd64.tar.gz buildkit-v0.11.6.linux-amd64.tar.gz 56 | pushd ./bin 57 | sudo nohup ./buildkitd & 58 | popd 59 | sleep 5 60 | sudo ./copa patch -i ${{ vars.ACR_NAME }}.azurecr.io/azure-voting-app-rust:${{ env.tag }} -r patch.json -t ${{ env.tag }} 61 | 62 | - name: Push patched Docker image 63 | id: img 64 | run: | 65 | echo "DIGEST=$(docker push ${{ vars.ACR_NAME }}.azurecr.io/azure-voting-app-rust:${{ env.tag }} | grep -oE 'sha256:[a-f0-9]{64}')" >> $GITHUB_OUTPUT 66 | 67 | - name: Setup Notation 68 | uses: notaryproject/notation-action/setup@v1 69 | with: 70 | version: "1.0.0" 71 | 72 | - name: Notation Sign 73 | uses: notaryproject/notation-action/sign@v1 74 | with: 75 | plugin_name: azure-kv 76 | plugin_url: https://github.com/Azure/notation-azure-kv/releases/download/v1.0.1/notation-azure-kv_1.0.1_linux_amd64.tar.gz 77 | plugin_checksum: f8a75d9234db90069d9eb5660e5374820edf36d710bd063f4ef81e7063d3810b 78 | key_id: ${{ vars.KEY_ID }} 79 | target_artifact_reference: ${{ vars.ACR_NAME }}.azurecr.io/azure-voting-app-rust@${{ steps.img.outputs.DIGEST }} 80 | signature_format: cose 81 | plugin_config: |- 82 | name=${{ vars.CERT_NAME }} 83 | self_signed=false -------------------------------------------------------------------------------- /.github/workflows/patch.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | patch: 8 | runs-on: ubuntu-latest 9 | 10 | #TODO: What if the image has already been patched? How do we know? 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | # provide relevant list of images to scan on each run 15 | images: ['s3cexampleacr.azurecr.io/azure-voting-app-rust:v0.1.0-alpha'] # add more for microservices TODO later 16 | 17 | steps: 18 | - name: Login to Azure 19 | id: login 20 | uses: azure/login@v1 21 | with: 22 | creds: ${{ secrets.AZURE_CREDENTIALS }} 23 | 24 | - name: Login to Azure Container Registry 25 | run: | 26 | az acr login --name ${{ vars.ACR_NAME }} 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v3.0.0 30 | 31 | - name: Generate Trivy Report 32 | uses: aquasecurity/trivy-action@69cbbc0cbbf6a2b0bab8dcf0e9f2d7ead08e87e4 33 | with: 34 | scan-type: 'image' 35 | format: 'json' 36 | output: 'report.json' 37 | ignore-unfixed: true 38 | vuln-type: 'os' 39 | image-ref: ${{ matrix.images }} 40 | 41 | - name: Check Vuln Count 42 | id: vuln_count 43 | run: | 44 | report_file="report.json" 45 | vuln_count=$(jq '.Results | length' "$report_file") 46 | echo "vuln_count=$vuln_count" >> $GITHUB_OUTPUT 47 | 48 | - name: Extract Patched Tag 49 | id: extract_tag 50 | run: | 51 | imageName=$(echo ${{ matrix.images }} | cut -d ':' -f1) 52 | current_tag=$(echo ${{ matrix.images }} | cut -d ':' -f2) 53 | 54 | if [[ $current_tag == *-[0-9] ]]; then 55 | numeric_tag=$(echo "$current_tag" | awk -F'-' '{print $NF}') 56 | non_numeric_tag=$(echo "$current_tag" | sed "s#-$numeric_tag##g") 57 | incremented_tag=$((numeric_tag+1)) 58 | new_tag="$non_numeric_tag-$incremented_tag" 59 | else 60 | new_tag="$current_tag-1" 61 | fi 62 | 63 | echo "patched_tag=$new_tag" >> $GITHUB_OUTPUT 64 | echo "imageName=$imageName" >> $GITHUB_OUTPUT 65 | 66 | - name: Copa Action 67 | if: steps.vuln_count.outputs.vuln_count != '0' 68 | id: copa 69 | uses: project-copacetic/copa-action@v1.0.0 70 | with: 71 | image: ${{ matrix.images }} 72 | image-report: 'report.json' 73 | patched-tag: ${{ steps.extract_tag.outputs.patched_tag }} 74 | buildkit-version: 'v0.11.6' 75 | # optional, default is latest 76 | copa-version: '0.3.0' 77 | 78 | - name: Docker Push Patched Image 79 | id: push 80 | if: steps.login.conclusion == 'success' 81 | run: | 82 | # docker push ${{ steps.copa.outputs.patched-image }} 83 | echo "DIGEST=$(docker push ${{ steps.copa.outputs.patched-image }} | grep -oE 'sha256:[a-f0-9]{64}')" >> $GITHUB_OUTPUT 84 | 85 | - name: Setup Notation 86 | if: steps.push.conclusion == 'success' 87 | uses: notaryproject/notation-action/setup@v1 88 | with: 89 | version: "1.0.0" 90 | 91 | - name: Notation Sign 92 | if: steps.push.conclusion == 'success' 93 | uses: notaryproject/notation-action/sign@v1 94 | with: 95 | plugin_name: azure-kv 96 | plugin_url: https://github.com/Azure/notation-azure-kv/releases/download/v1.0.1/notation-azure-kv_1.0.1_linux_amd64.tar.gz 97 | plugin_checksum: f8a75d9234db90069d9eb5660e5374820edf36d710bd063f4ef81e7063d3810b 98 | key_id: ${{ vars.KEY_ID }} 99 | target_artifact_reference: ${{ steps.extract_tag.outputs.imageName }}@${{ steps.push.outputs.DIGEST }} 100 | signature_format: cose 101 | plugin_config: |- 102 | name=${{ vars.CERT_NAME }} 103 | self_signed=false -------------------------------------------------------------------------------- /docs/bonus.md: -------------------------------------------------------------------------------- 1 | # Build a secure pipeline with GitHub Actions 2 | 3 | 4 | 5 | In this workshop, you will learn how to build a secure pipeline using GitHub Actions to build, scan, sign, and deploy the Azure Voting app to your Azure Kubernetes Service cluster. To get started, make sure to fork this repository to your GitHub account. 6 | 7 | ## Login into the GitHub CLI 8 | 9 | First, you'll need to sign into the GitHub CLI. Run the following command and follow the prompts to sign in: 10 | 11 | ```bash 12 | gh auth login 13 | ``` 14 | 15 |
16 | 17 | > TIP: There are several ways to authenticate with the GitHub CLI. To help you choose the best option for your environment, see the [GitHub CLI authentication documentation](https://cli.github.com/manual/gh_auth_login). 18 | 19 |
20 | 21 | ## Refresh Azure resources variables 22 | 23 | Next, refresh the environment variables for the Azure resources created earlier in this workshop. Run the following commands to refresh the environment variables: 24 | 25 | 26 | ```bash 27 | export SUBSCRIPTION_ID=$(az account show --query id --output tsv); 28 | cd terraform/; 29 | export GROUP_NAME="$(terraform output -raw rg_name)" 30 | export AKV_NAME="$(terraform output -raw akv_name)" 31 | export ACR_NAME="$(terraform output -raw acr_name)" 32 | export CERT_NAME="$(terraform output -raw cert_name)" 33 | cd .. 34 | ``` 35 | 36 | ## Create an Azure Service Principal 37 | 38 | First, you'll need to create a Service Principal for the GitHub Actions workflow to use to authenticate to Azure. 39 | 40 | Run the following command to create a Service Principal: 41 | 42 | 43 | ```bash 44 | spDisplayName='github-workflow-sp'; 45 | credJSON=$(az ad sp create-for-rbac --name $spDisplayName --role contributor \ 46 | --scopes /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$GROUP_NAME \ 47 | --sdk-auth) 48 | ``` 49 | 50 |
51 | 52 | Example Output 53 | 54 | ```output 55 | { 56 | "clientId": "00000000-0000-0000-0000-000000000000", 57 | "clientSecret": "00000000-0000-0000-0000-000000000000", 58 | "subscriptionId": "00000000-0000-0000-0000-000000000000", 59 | "tenantId": "00000000-0000-0000-0000-000000000000", 60 | "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", 61 | "resourceManagerEndpointUrl": "https://management.azure.com/", 62 | "activeDirectoryGraphResourceId": "https://graph.windows.net/", 63 | "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", 64 | "galleryEndpointUrl": "https://gallery.azure.com/", 65 | "managementEndpointUrl": "https://management.core.windows.net/" 66 | } 67 | ``` 68 | 69 |
70 | 71 | 72 | Next, you'll need to create an access policy for the Service Principal that grants key sign and secert get permssions on the Azure Key Vault instance. 73 | 74 | Run the following command to create an access policy for the Service Principal: 75 | 76 | ```bash 77 | objectId=$(az ad sp list --display-name $spDisplayName --query '[].id' --output tsv); 78 | az keyvault set-policy --name $AKV_NAME --object-id $objectId --certificate-permissions get --key-permissions sign --secret-permissions get 79 | ``` 80 | 81 | > NOTE: These permissions are used to get the secret from the signing certificate. 82 | 83 | ### Create the AZURE_CREDENTIALS secret 84 | 85 | Create a Service Principal for the GitHub Actions workflow to authenticate with Azure. Execute the following command to create a Service Principal: 86 | 87 | ```bash 88 | gh secret set AZURE_CREDENTIALS --body "$credJSON" 89 | ``` 90 | 91 | ### Create Azure resource variables 92 | 93 | Create the following GitHub secrets to store the Azure resource variables: 94 | 95 | ```bash 96 | 97 | gh variable set ACR_NAME --body "$ACR_NAME"; 98 | gh variable set CERT_NAME --body "$CERT_NAME"; 99 | gh variable set KEY_ID --body "$KEY_ID"; 100 | ``` 101 | 102 | ### Trigger the GitHub Actions workflow 103 | 104 | Now that you've modified the GitHub Actions workflow, you can trigger it by pushing a change to the repository. 105 | 106 | Run the following command to push a change to the repository: 107 | 108 | ```bash 109 | #Push a change to start the build process 110 | echo >> README.md 111 | git add .; git commit -m 'Demo: Lets do it live!'; git push 112 | ``` 113 | 114 | Browse to the `Actions` tab in your repository and you should see the workflow running. Wait for the workflow to complete. Then check the logs for the message 'Successfully signed...' in the logs to confirm the image was signed. 115 | 116 | --- 117 | -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | . ./util.sh 2 | 3 | run 'clear' 4 | 5 | desc 'Loading demo environment variables...' 6 | export IMAGE='azure-voting-app-rust:v0.1-alpha' 7 | cd terraform/ 8 | export GROUP_NAME="$(terraform output -raw rg_name)" 9 | export AKS_NAME="$(terraform output -raw aks_name)" 10 | export VAULT_URI="$(terraform output -raw akv_uri)" 11 | export KEYVAULT_NAME="$(terraform output -raw akv_name)" 12 | export ACR_NAME="$(terraform output -raw acr_name)" 13 | export CERT_NAME="$(terraform output -raw cert_name)" 14 | export TENANT_ID="$(terraform output -raw tenant_id)" 15 | export CLIENT_ID="$(terraform output -raw wl_client_id)" 16 | cd .. 17 | az acr login --name $ACR_NAME >> /dev/null 2>&1 18 | 19 | desc 'Build & pull container images' 20 | run 'docker build -t azure-voting-app-rust:v0.1-alpha .' 21 | run 'docker pull postgres:15.0-alpine' 22 | 23 | desc 'List Docker images' 24 | run 'docker images' 25 | 26 | desc 'Scan the azure-voting-app-rust images' 27 | run 'sudo trivy image azure-voting-app-rust:v0.1-alpha' 28 | 29 | desc 'Adjust severity levels' 30 | run "sudo trivy image --severity CRITICAL ${IMAGE}" 31 | 32 | desc 'Filter vulnerabilities by type' 33 | run "sudo trivy image --vuln-type os --severity CRITICAL ${IMAGE}" 34 | 35 | desc 'Export a vulnerability report' 36 | run "sudo trivy image --exit-code 0 --format json --output ./patch.json --scanners vuln --vuln-type os --ignore-unfixed ${IMAGE}" 37 | 38 | desc 'Review vulnerable packages found in the image' 39 | run "cat patch.json | jq '.Results[0].Vulnerabilities[] | .PkgID' | sort | uniq" 40 | 41 | desc 'Tag and push azure-voting-app-rust to ACR' 42 | ACR_IMAGE=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha 43 | run "docker tag ${IMAGE} ${ACR_IMAGE}" 44 | run "docker push ${ACR_IMAGE}" 45 | 46 | sudo ./bin/buildkitd &> /dev/null & # why does this still output? 47 | desc "Patch container image with Copacetic" 48 | run "sudo copa patch -i ${ACR_IMAGE} -r ./patch.json -t v0.1-alpha-1" 49 | sudo pkill buildkitd >> /dev/null 2>&1 50 | ACR_IMAGE_PATCHED=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha-1 51 | 52 | desc "Re-scan the patched image" 53 | run "sudo trivy image --severity CRITICAL --scanners vuln ${ACR_IMAGE_PATCHED}" 54 | 55 | desc "Push the patched image to ACR" 56 | run "docker push ${ACR_IMAGE_PATCHED}" 57 | 58 | desc 'Tag and push Postgres image to ACR' 59 | run "docker tag postgres:15.0-alpine ${ACR_NAME}.azurecr.io/postgres:15.0-alpine" 60 | run "docker push ${ACR_NAME}.azurecr.io/postgres:15.0-alpine" 61 | 62 | KEY_ID=$(az keyvault certificate show --name $CERT_NAME --vault-name $KEYVAULT_NAME --query kid -o tsv) 63 | desc 'Add a signing key to Notation CLI' 64 | run "notation key add --plugin azure-kv ${CERT_NAME} --id ${KEY_ID} --default" 65 | 66 | desc 'List the Notation key' 67 | run 'notation key list' 68 | 69 | desc 'Get the Docker image digest' 70 | run "docker image inspect --format='{{index .RepoDigests 0}}' ${ACR_IMAGE_PATCHED}" 71 | run "docker image inspect --format='{{index .RepoDigests 1}}' ${ACR_NAME}.azurecr.io/postgres:15.0-alpine" 72 | export APP_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $ACR_IMAGE_PATCHED) 73 | export DB_DIGEST=$(docker image inspect --format='{{range $digest := .RepoDigests}}{{println $digest}}{{end}}' ${ACR_NAME}.azurecr.io/postgres:15.0-alpine | sort | tail -1) 74 | 75 | desc 'Sign the azure-voting-app-rust image' 76 | run "notation sign ${APP_DIGEST}" 77 | 78 | desc 'Sign the Postgres image' 79 | run "notation sign ${DB_DIGEST}" 80 | 81 | az aks get-credentials --resource-group ${GROUP_NAME} --name ${AKS_NAME} 82 | desc 'Deploy unsigned app images' 83 | run "kubectl apply -f manifests/" 84 | kubectl delete manifest/ >> /dev/null 2>&1 85 | 86 | # desc 'Check Ratify logs for blocked pod deployment' 87 | # run "kubectl logs deployment/ratify --namespace gatekeeper-system | grep voting" 88 | 89 | desc "Modify the app deployment manifests to use the signed image" 90 | run "sed -i \"s|azure-voting-app-rust:v0.1-alpha|${APP_DIGEST}|\" ./manifests/deployment-app.yaml" 91 | run "code ./manifests/deployment-app.yaml" 92 | 93 | desc "Modify the db deployment manifests to use the signed image" 94 | run "sed -i \"s|postgres:15.0-alpine|${DB_DIGEST}|\" ./manifests/deployment-db.yaml" 95 | run "code ./manifests/deployment-db.yaml" 96 | 97 | desc 'Deploy signed app images' 98 | run "kubectl apply -f manifests/" 99 | 100 | desc 'Sleep 10 seconds, wait for pods' 101 | sleep 10 102 | desc 'Check deployment status' 103 | run "kubectl get pods" 104 | 105 | desc 'Review Ratify constraints' 106 | run "code ./manifests/constraint.yaml" 107 | 108 | desc 'Review Ratify template' 109 | run "code ./manifests/template.yaml" 110 | 111 | desc 'Check ingress status' 112 | run "kubectl get ingress" 113 | 114 | desc 'Test the app' 115 | 116 | desc 'Game Over: End of demo' -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "3.52.0" 6 | } 7 | azuread = { 8 | source = "hashicorp/azuread" 9 | version = "2.28.0" 10 | } 11 | } 12 | } 13 | 14 | provider "azurerm" { 15 | features { 16 | resource_group { 17 | prevent_deletion_if_contains_resources = false 18 | } 19 | } 20 | } 21 | 22 | data "azurerm_client_config" "current" {} 23 | 24 | data "azuread_client_config" "current" {} 25 | 26 | resource "azurerm_resource_group" "rg" { 27 | name = var.resource_group_name 28 | location = var.location 29 | } 30 | 31 | resource "azurerm_container_registry" "registry" { 32 | name = var.registry_name 33 | resource_group_name = azurerm_resource_group.rg.name 34 | location = azurerm_resource_group.rg.location 35 | sku = "Standard" 36 | anonymous_pull_enabled = true # Copacetic limitation 37 | } 38 | 39 | resource "azurerm_user_assigned_identity" "identity" { 40 | name = var.identity_name 41 | resource_group_name = azurerm_resource_group.rg.name 42 | location = azurerm_resource_group.rg.location 43 | tags = var.tags 44 | } 45 | 46 | resource "azurerm_role_assignment" "acr" { 47 | scope = azurerm_container_registry.registry.id 48 | role_definition_name = "AcrPull" 49 | principal_id = azurerm_user_assigned_identity.identity.principal_id 50 | } 51 | 52 | resource "azurerm_role_assignment" "acr-admin" { 53 | scope = azurerm_container_registry.registry.id 54 | role_definition_name = "Owner" 55 | principal_id = data.azuread_client_config.current.object_id 56 | } 57 | 58 | resource "azurerm_key_vault" "kv" { 59 | name = var.key_vault_name 60 | location = azurerm_resource_group.rg.location 61 | resource_group_name = azurerm_resource_group.rg.name 62 | tenant_id = data.azurerm_client_config.current.tenant_id 63 | sku_name = "standard" 64 | tags = var.tags 65 | 66 | access_policy { 67 | tenant_id = data.azurerm_client_config.current.tenant_id 68 | object_id = data.azuread_client_config.current.object_id 69 | 70 | key_permissions = [ 71 | "Get", "Sign" 72 | ] 73 | 74 | secret_permissions = [ 75 | "Get", "Set", "List", "Delete", "Purge" 76 | ] 77 | 78 | certificate_permissions = [ 79 | "Get", "Create", "Delete", "Purge" 80 | ] 81 | } 82 | } 83 | 84 | resource "azurerm_key_vault_access_policy" "workload-identity" { 85 | key_vault_id = azurerm_key_vault.kv.id 86 | tenant_id = data.azurerm_client_config.current.tenant_id 87 | object_id = azurerm_user_assigned_identity.identity.principal_id 88 | 89 | certificate_permissions = [ 90 | "Get" 91 | 92 | ] 93 | secret_permissions = [ 94 | "Get" 95 | ] 96 | } 97 | 98 | 99 | resource "azurerm_kubernetes_cluster" "aks" { 100 | name = var.cluster_name 101 | location = azurerm_resource_group.rg.location 102 | resource_group_name = azurerm_resource_group.rg.name 103 | dns_prefix = "${var.cluster_name}-dns" 104 | kubernetes_version = "1.28.3" 105 | workload_identity_enabled = true 106 | oidc_issuer_enabled = true 107 | automatic_channel_upgrade = "node-image" 108 | 109 | 110 | default_node_pool { 111 | name = "default" 112 | node_count = 3 113 | vm_size = "Standard_D2_v2" 114 | 115 | } 116 | 117 | web_app_routing { 118 | dns_zone_id = "" 119 | } 120 | 121 | 122 | identity { 123 | type = "SystemAssigned" 124 | } 125 | 126 | tags = var.tags 127 | } 128 | 129 | resource "azurerm_role_assignment" "linkAcrAks" { 130 | principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id 131 | role_definition_name = "AcrPull" 132 | scope = azurerm_container_registry.registry.id 133 | skip_service_principal_aad_check = true 134 | } 135 | 136 | 137 | resource "azurerm_federated_identity_credential" "workload-identity-credential" { 138 | name = "ratify-federated-credential" 139 | resource_group_name = azurerm_resource_group.rg.name 140 | audience = ["api://AzureADTokenExchange"] 141 | issuer = azurerm_kubernetes_cluster.aks.oidc_issuer_url 142 | parent_id = azurerm_user_assigned_identity.identity.id 143 | subject = "system:serviceaccount:${var.ratify_namespace}:ratify-admin" 144 | } 145 | 146 | resource "azurerm_key_vault_certificate" "sign-cert" { 147 | name = var.sign_cert_name 148 | key_vault_id = azurerm_key_vault.kv.id 149 | 150 | certificate_policy { 151 | issuer_parameters { 152 | name = "Self" 153 | } 154 | 155 | key_properties { 156 | exportable = false 157 | key_size = 2048 158 | key_type = "RSA" 159 | reuse_key = true 160 | } 161 | 162 | lifetime_action { 163 | action { 164 | action_type = "AutoRenew" 165 | } 166 | 167 | trigger { 168 | days_before_expiry = 30 169 | } 170 | } 171 | 172 | secret_properties { 173 | content_type = "application/x-pem-file" 174 | } 175 | 176 | x509_certificate_properties { 177 | extended_key_usage = ["1.3.6.1.5.5.7.3.3"] 178 | 179 | key_usage = [ 180 | "digitalSignature", 181 | ] 182 | 183 | subject = "CN=example.com,O=Notation,L=Seattle,ST=WA,C=US" 184 | validity_in_months = 12 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | ## Inftrastructure Overview 2 | 3 | As part of this workshop, you'll need to deploy the following Azure resources: 4 | | Azure Resources | Notes | 5 | |------------------------------------|-------------------------------------------------------------------------------------------------------| 6 | | Azure Resource Group | | 7 | | Azure Key Vault | Stores the certificate used for digtial signatures | 8 | | Azure Container Registry | | 9 | | Azure Kubernetes Service | | 10 | | Azure User Assigned Managed Identity| Authenticates to Azure Key Vault as a Workload Identity and Azure Container Registry by using the AcrPull role. | 11 | 12 | ## Deploy the Azure resources with Terraform 13 | 14 | First, log into Azure with the Azure CLI. 15 | 16 | ```bash 17 | az login 18 | ``` 19 | 20 | Next, deploy the Terraform configuration. This will create the Azure resources needed for this workshop. 21 | 22 | ```bash 23 | cd terraform; 24 | terraform init; 25 | terraform apply 26 | ``` 27 | 28 |
29 | Example Output 30 | 31 | ```output 32 | azurerm_resource_group.rg: Creating... 33 | azurerm_resource_group.rg: Creation complete after 1s [id=/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg] 34 | azurerm_key_vault.kv: Creating... 35 | azurerm_key_vault.kv: Creation complete after 4s [id=https://kv.vault.azure.net] 36 | azurerm_user_assigned_identity.ua: Creating... 37 | azurerm_user_assigned_identity.ua: Creation complete after 1s [id=/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ua] 38 | azurerm_container_registry.acr: Creating... 39 | ``` 40 | 41 |
42 | 43 | 44 |
45 | 46 | > Certain Azure resources need to be globally unique. If you receive an error that a resource already exists, you may need to change the name of the resource in the `terraform.tfvars` file. 47 | 48 |
49 | 50 | Run the following command to export the Terraform output as environment variables: 51 | 52 | ```bash 53 | export GROUP_NAME="$(terraform output -raw rg_name)" 54 | export AKS_NAME="$(terraform output -raw aks_name)" 55 | export VAULT_URI="$(terraform output -raw akv_uri)" 56 | export AKV_NAME="$(terraform output -raw akv_name)" 57 | export ACR_NAME="$(terraform output -raw acr_name)" 58 | export CERT_NAME="$(terraform output -raw cert_name)" 59 | export TENANT_ID="$(terraform output -raw tenant_id)" 60 | export CLIENT_ID="$(terraform output -raw wl_client_id)" 61 | ``` 62 | 63 | Before continuing, change back to the root of the repository. 64 | 65 | ```bash 66 | cd .. 67 | ``` 68 | 69 | ### Deploy Ratify to the AKS cluster 70 | 71 | Gatekeeper is an open-source project from the CNCF that allows you to enforce policies on your Kubernetes cluster and Ratify is a tool that allows you to deploy policies and constraints that prevent unsigned container image from being deployed to Kubernetes. 72 | 73 | Run the following command to get the Kubernetes credentials for your cluster: 74 | 75 | ```bash 76 | az aks get-credentials --resource-group ${GROUP_NAME} --name ${AKS_NAME} 77 | ``` 78 | 79 | Next, run the following command to deploy Gatekeeper to your cluster: 80 | 81 | ```bash 82 | helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts 83 | 84 | helm install gatekeeper/gatekeeper \ 85 | --name-template=gatekeeper \ 86 | --namespace gatekeeper-system --create-namespace \ 87 | --set enableExternalData=true \ 88 | --set validatingWebhookTimeoutSeconds=5 \ 89 | --set mutatingWebhookTimeoutSeconds=2 90 | ``` 91 | 92 | Run the following command to deploy Ratify to your cluster: 93 | 94 | ```bash 95 | helm repo add ratify https://deislabs.github.io/ratify 96 | 97 | helm install ratify \ 98 | ratify/ratify --atomic \ 99 | --namespace gatekeeper-system \ 100 | --set akvCertConfig.enabled=true \ 101 | --set featureFlags.RATIFY_CERT_ROTATION=true \ 102 | --set akvCertConfig.vaultURI=${VAULT_URI} \ 103 | --set akvCertConfig.cert1Name=${CERT_NAME} \ 104 | --set akvCertConfig.tenantId=${TENANT_ID} \ 105 | --set oras.authProviders.azureWorkloadIdentityEnabled=true \ 106 | --set azureWorkloadIdentity.clientId=${CLIENT_ID} 107 | ``` 108 | 109 | Once Ratify is deployed, you'll need to deploy the policies and constraints that prevent unsigned container images from being deployed to Kubernetes. 110 | 111 | Run the following command to deploy the Ratify policies to your cluster: 112 | 113 | 114 | ```bash 115 | kubectl apply -f manifests/template.yaml 116 | kubectl apply -f manifests/constraint.yaml 117 | ``` 118 | 119 | Verify Ratify is running with the following command: 120 | 121 | ```bash 122 | kubectl get pods --namespace gatekeeper-system 123 | ``` 124 | 125 | 126 | Example Output 127 | 128 | ```output 129 | NAME READY STATUS RESTARTS AGE 130 | gatekeeper-audit-769879bb55-bdsr5 1/1 Running 1 (34m ago) 34m 131 | gatekeeper-controller-manager-d8c9c5cd5-bstmb 1/1 Running 0 34m 132 | gatekeeper-controller-manager-d8c9c5cd5-dzk2f 1/1 Running 0 34m 133 | gatekeeper-controller-manager-d8c9c5cd5-qftxt 1/1 Running 0 34m 134 | ratify-88b59894d-w5nxl 1/1 Running 0 31m 135 | ``` 136 | 137 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | mod database; 5 | mod model; 6 | mod schema; 7 | use crate::schema::votes::vote_value; 8 | use actix_files::Files; 9 | use actix_web::{middleware::Logger, post, web, App, HttpResponse, HttpServer}; 10 | use database::setup; 11 | use diesel::{dsl::*, pg::PgConnection, prelude::*, r2d2::ConnectionManager}; 12 | use env_logger::Env; 13 | use handlebars::Handlebars; 14 | use log::info; 15 | use model::NewVote; 16 | use r2d2::Pool; 17 | use schema::votes::dsl::votes; 18 | use serde::Deserialize; 19 | use serde_json::json; 20 | use std::env::var; 21 | use std::fmt; 22 | use std::sync::Mutex; 23 | 24 | 25 | lazy_static! { 26 | static ref FIRST_VALUE: String = var("FIRST_VALUE").unwrap_or("Dogs".to_string()); 27 | static ref SECOND_VALUE: String = var("SECOND_VALUE").unwrap_or("Cats".to_string()); 28 | } 29 | 30 | #[derive(Debug, Deserialize)] 31 | enum VoteValue { 32 | FirstValue, 33 | SecondValue, 34 | Reset, 35 | } 36 | 37 | impl fmt::Display for VoteValue { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | match self { 40 | VoteValue::SecondValue => write!(f, "{}", *SECOND_VALUE), 41 | VoteValue::FirstValue => write!(f, "{}", *FIRST_VALUE), 42 | VoteValue::Reset => write!(f, "Reset"), 43 | } 44 | } 45 | } 46 | 47 | impl VoteValue { 48 | fn source_value(input: &str) -> VoteValue { 49 | if input == *FIRST_VALUE { 50 | return VoteValue::FirstValue 51 | } 52 | else if input == *SECOND_VALUE { 53 | return VoteValue::SecondValue 54 | } 55 | else if input == "Reset" { 56 | return VoteValue::Reset 57 | } 58 | else { 59 | panic!("Failed to match the vote type from {}", input); 60 | }; 61 | 62 | } 63 | } 64 | 65 | #[derive(Deserialize)] 66 | struct FormData { 67 | vote: String, 68 | } 69 | 70 | struct AppStateVoteCounter { 71 | first_value_counter: Mutex, // <- Mutex is necessary to mutate safely across threads 72 | second_value_counter: Mutex, 73 | } 74 | 75 | /// extract form data using serde 76 | /// this handler gets called only if the content type is *x-www-form-urlencoded* 77 | /// and the content of the request could be deserialized to a `FormData` struct 78 | #[post("/")] 79 | async fn submit( 80 | form: web::Form, 81 | data: web::Data, 82 | pool: web::Data>>, 83 | hb: web::Data>, 84 | ) -> HttpResponse { 85 | let mut first_value_counter = data.first_value_counter.lock().unwrap(); // <- get counter's MutexGuard 86 | let mut second_value_counter = data.second_value_counter.lock().unwrap(); 87 | 88 | info!("Vote is: {}", &form.vote); 89 | info!("Debug Vote is: {:?}", &form.vote); 90 | 91 | let vote = VoteValue::source_value(&form.vote); 92 | 93 | match vote { 94 | VoteValue::FirstValue => *first_value_counter += 1, // <- access counter inside MutexGuard 95 | VoteValue::SecondValue => *second_value_counter += 1, 96 | VoteValue::Reset => { 97 | *first_value_counter = 0; 98 | *second_value_counter = 0; 99 | } 100 | } 101 | 102 | let data = json!({ 103 | "title": "Azure Voting App", 104 | "button1": VoteValue::FirstValue.to_string(), 105 | "button2": VoteValue::SecondValue.to_string(), 106 | "value1": first_value_counter.to_string(), 107 | "value2": second_value_counter.to_string() 108 | }); 109 | 110 | let body = hb.render("index", &data).unwrap(); 111 | 112 | // if the vote value is not reset then save the 113 | if !matches!(vote, VoteValue::Reset) { 114 | let vote_data = NewVote { 115 | vote_value: form.vote.to_string(), 116 | }; 117 | 118 | let mut connection = pool.get().unwrap(); 119 | let _vote_data = web::block(move || { 120 | diesel::insert_into(votes) 121 | .values(vote_data) 122 | .execute(&mut connection) 123 | }) 124 | .await; 125 | } else { 126 | let mut connection = pool.get().unwrap(); 127 | let _vote_data = web::block(move || { 128 | let _ = diesel::delete(votes).execute(&mut connection); 129 | }) 130 | .await; 131 | } 132 | 133 | HttpResponse::Ok().body(body) 134 | } 135 | 136 | async fn index( 137 | data: web::Data, 138 | hb: web::Data>, 139 | ) -> HttpResponse { 140 | let first_value_counter = data.first_value_counter.lock().unwrap(); // <- get counter's MutexGuard 141 | let second_value_counter = data.second_value_counter.lock().unwrap(); 142 | 143 | info!("Value 1: {}", VoteValue::FirstValue); 144 | info!("Value 2: {}", VoteValue::SecondValue); 145 | 146 | let data = json!({ 147 | "title": "Azure Voting App", 148 | "button1": VoteValue::FirstValue.to_string(), 149 | "button2": VoteValue::SecondValue.to_string(), 150 | "value1": first_value_counter.to_string(), 151 | "value2": second_value_counter.to_string() 152 | }); 153 | let body = hb.render("index", &data).unwrap(); 154 | HttpResponse::Ok().body(body) 155 | } 156 | 157 | #[actix_web::main] 158 | async fn main() -> std::io::Result<()> { 159 | // Default logging format is: 160 | // %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T 161 | env_logger::init_from_env(Env::default().default_filter_or("info")); 162 | 163 | let pool = setup(); 164 | let mut connection = pool.get().unwrap(); 165 | 166 | // Load up the dog votes 167 | let first_value_query = votes.filter(vote_value.eq(FIRST_VALUE.clone())); 168 | let first_value_result = first_value_query.select(count(vote_value)).first(&mut connection); 169 | let first_value_count = first_value_result.unwrap_or(0); 170 | 171 | // Load up the cat votes 172 | let second_value_query = votes.filter(vote_value.eq(SECOND_VALUE.clone())); 173 | let second_value_result = second_value_query.select(count(vote_value)).first(&mut connection); 174 | let second_value_count = second_value_result.unwrap_or(0); 175 | 176 | // Note: web::Data created _outside_ HttpServer::new closure 177 | let vote_counter = web::Data::new(AppStateVoteCounter { 178 | first_value_counter: Mutex::new(first_value_count), 179 | second_value_counter: Mutex::new(second_value_count), 180 | }); 181 | 182 | let mut handlebars = Handlebars::new(); 183 | handlebars 184 | .register_templates_directory(".html", "./static/") 185 | .unwrap(); 186 | let handlebars_ref = web::Data::new(handlebars); 187 | 188 | info!("Listening on port 8080"); 189 | HttpServer::new(move || { 190 | App::new() 191 | .wrap(Logger::default()) 192 | // .wrap(Logger::new("%a %{User-Agent}i")) // <- optionally create your own format 193 | .app_data(vote_counter.clone()) // <- register the created data 194 | .app_data(handlebars_ref.clone()) 195 | .data(pool.clone()) 196 | .service(Files::new("/static", "static").show_files_listing()) 197 | .route("/", web::get().to(index)) 198 | .service(submit) 199 | }) 200 | .bind(("0.0.0.0", 8080))? 201 | .run() 202 | .await 203 | } 204 | -------------------------------------------------------------------------------- /docs/workshop.md: -------------------------------------------------------------------------------- 1 | --- 2 | short_title: Securing container deployments on Azure Kubernetes Service with open-source tools 3 | description: Learn how to use open-source tools to secure your container deployments on Azure Kubernetes Service. 4 | type: workshop 5 | authors: Josh Duffney 6 | contacts: '@joshduffney' 7 | # banner_url: assets/copilot-banner.jpg 8 | duration_minutes: 30 9 | audience: devops engineers, devs, site reliability engineers, security engineers 10 | level: intermediate 11 | tags: azure, github actions, notary, ratify, secure supply chain, kubernetes, helm, terraform, gatekeeper, azure kubernetes service, azure key vault, azure container registry 12 | published: false 13 | wt_id: 14 | sections_title: 15 | - Introduction 16 | --- 17 | 18 | # Securing container deployments on Azure Kubernetes Service by using open-source tools 19 | 20 | In this workshop, you'll learn how to use open-source tools; Trivy, Copacetic, Notary, and Ratify to secure your container deployments on Azure Kubernetes Service. 21 | 22 | ![Secure Supply Chain](/imgs/secure-supply-chain-on-aks-overview.png) 23 | 24 | ## Objectives 25 | 26 | You'll learn how to: 27 | - Use Trivy to scan container images for vulnerabilities 28 | - Automate container image patching with Copacetic 29 | - Sign container images with Notation 30 | - Prevent unsigned container images from being deployed with Ratify 31 | 32 | ## Prerequisites 33 | 34 | | | | 35 | |----------------------|------------------------------------------------------| 36 | | GitHub account | [Get a free GitHub account](https://github.com/join) | 37 | | Azure account | [Get a free Azure account](https://azure.microsoft.com/free) | 38 | | Visual Studio Code | [Install VS Code](https://code.visualstudio.com/download) | 39 | 40 | 41 | In order to begin this workshop, you'll need to setup the Azure environment and deploy Gatekeeper and Ratify to your AKS cluster. 42 | 43 | Follow the [setup](setup.md) instructions to deploy and configure the infrastructure. 44 | 45 | --- 46 | 47 | ## Start the dev container 48 | 49 | A local development environment is provided for this workshop using a dev container. It includes all the tools you need to successfully participate in the workshop. 50 | 51 | Follow the steps below to fork the repository and open it in VS Code: 52 | 53 | 1. Fork the repository by navigating to the original repository URL: https://github.com/duffney/secure-supply-chain-on-aks.git. Click on the "Fork" button in the top right corner of the GitHub page. This will create a copy of the repository under your GitHub account. 54 | 55 | 2. Once the repository is forked, navigate to your forked repository on GitHub. The URL should be https://github.com/your-username/secure-supply-chain-on-aks.git, where "your-username" is your GitHub username. 56 | 57 | 3. Click on the "Code" button, and copy the URL provided (which will be the URL of your forked repository). 58 | 59 | 4. Open your terminal or command prompt and run the following command to clone your forked repository: 60 | 61 | ```bash 62 | git clone https://github.com/your-username/secure-supply-chain-on-aks.git 63 | ``` 64 | 65 | 5. Change the working directory to the cloned repository: 66 | 67 | ```bash 68 | cd secure-supply-chain-on-aks 69 | ``` 70 | 71 | 6. Open the repository in VS Code: 72 | 73 | ```bash 74 | code . 75 | ``` 76 | 77 | 7. VS Code will prompt you to reopen the repository in a dev container. Click **Reopen in Container**. This will take a few minutes to build the dev container. 78 | 79 |
80 | 81 | > If you don't see the prompt, you can open the command palette by hitting `Ctrl+Shift+P` on Windows or `Cmd+Shift+P` on Mac and search for **Dev Containers: Reopen in Container**. 82 | 83 |
84 | 85 | --- 86 | 87 | ## Scanning container images for vulnerabilities 88 | 89 | 90 | 91 | Containerization has become an integral part of modern software development and deployment. However, with the increased adoption of containers, there comes a need for ensuring their security. 92 | 93 | In this section, you'll learn how to use Trivy to scan a container image for vulnerabilities. 94 | 95 | ### Build and pull the Azure Voting App container images 96 | 97 | Before you begin, there is a `Dockerfile` that will build a container image that hosts the Azure Voting App. 98 | 99 | The Azure Voting App is a simple Rust application that allows users to vote between the two options presented and stores the results in a database. 100 | 101 | Run the following command to build the Azure Voting web app container image 102 | 103 | ```bash 104 | docker build -t azure-voting-app-rust:v0.1-alpha . 105 | ``` 106 | 107 | Next pull the `PostgreSQL` container image from Docker Hub. This will be used to store the votes. 108 | 109 | ```bash 110 | docker pull postgres:15.0-alpine 111 | ``` 112 | 113 | ### Running Trivy for Vulnerability Scans 114 | 115 | Trivy is an open-source vulnerability scanner specifically designed for container images. It provides a simple and efficient way to detect vulnerabilities in containerized applications. 116 | 117 | Trivy leverages a comprehensive vulnerability database and checks container images against known security issues, including vulnerabilities in the operating system packages, application dependencies, and other components. 118 | 119 | Run the following command to scan the `azure-voting-app-rust` container image for vulnerabilities: 120 | 121 | ```bash 122 | IMAGE=azure-voting-app-rust:v0.1-alpha; 123 | trivy image $IMAGE; 124 | ``` 125 | 126 | 127 | Example Output 128 | 129 | ```output 130 | 2023-07-14T17:08:57.400Z INFO Vulnerability scanning is enabled 131 | 2023-07-14T17:08:57.401Z INFO Secret scanning is enabled 132 | 2023-07-14T17:08:57.401Z INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning 133 | 2023-07-14T17:08:57.401Z INFO Please see also https://aquasecurity.github.io/trivy/v0.41/docs/secret/scanning/#recommendation for faster secret detection 134 | 2023-07-14T17:08:57.418Z INFO Detected OS: debian 135 | 2023-07-14T17:08:57.418Z INFO Detecting Debian vulnerabilities... 136 | 2023-07-14T17:08:57.448Z INFO Number of language-specific files: 0 137 | 138 | azure-voting-app-rust:v0.1-alpha (debian 11.2) 139 | ============================================== 140 | Total: 154 (UNKNOWN: 0, LOW: 76, MEDIUM: 27, HIGH: 37, CRITICAL: 14) 141 | ........................................................... 142 | ........................................................... 143 | ``` 144 | 145 | 146 | 147 | Trivy will start scanning the specified container image for vulnerabilities. It will analyze the operating system, application packages, and libraries within the container to identify any known security issues. 148 | 149 | ### Adjusting Severity Levels 150 | 151 | Trivy allows you to customize the severity level of the reported vulnerabilities. By default, it provides information about vulnerabilities of all severity levels. However, you can narrow down the results based on your requirements. 152 | 153 | For example, if you only want to see vulnerabilities classified as CRITICAL, you can modify the command as follows: 154 | 155 | ```bash 156 | trivy image --severity CRITICAL $IMAGE 157 | ``` 158 | 159 | 160 | 161 | ### Filtering vulnerabilities by type 162 | 163 | By default, Trivy scans for vulnerabilities in all components of the container image, including the operating system packages, application dependencies, and libraries. However, you can narrow down the results by specifying the type of vulnerabilities you want to see. 164 | 165 | For example, if you only want to see vulnerabilities in the operating system packages, you can modify the command as follows: 166 | 167 | ```bash 168 | trivy image --vuln-type os --severity CRITICAL $IMAGE 169 | ``` 170 | 171 | ### Chosing a scanner 172 | 173 | Trivy supports four scanner options; vuln, config, secret, and license. Vuln is the default scanner and it scans for vulnerabilities in the container image. Config scans for misconfiguration in infrastructure as code configurations, like Terraform. Secret scans for sensitive information and secrets in the project files. And license scans for software license issues. 174 | 175 | You can specify the scanner you want to use by using the `--scanners` option. For example, if you only want to use the vuln scanner, you can modify the command as follows: 176 | 177 | ```bash 178 | trivy image --scanners vuln $IMAGE 179 | ``` 180 | 181 | ### Exporting a vulnerability report 182 | 183 | Seeing the vulnerability reports in the terminal is useful, but it's not the most convenient way to view the results. Trivy allows you to export the vulnerability report in a variety of formats, including JSON, HTML, and CSV. 184 | 185 | Run the following command to export the vulnerability report in JSON format: 186 | 187 | ```bash 188 | trivy image --exit-code 0 --format json --output ./patch.json --scanners vuln --vuln-type os --ignore-unfixed $IMAGE 189 | ``` 190 | 191 | Having a point-in-time report of the vulnerabilites discovered in your container is certainly a nice to have, but wouldn't it be even more awesome if that report was used to automatically add patched layers to the container image? 192 | 193 | --- 194 | 195 | ## Using Copacetic to patch container images 196 | 197 | Copacetic is an open-source tool that helps you patch container images for vulnerabilities. It uses Trivy vulnerability reports to identify the vulnerabilities in the container image and adds patched layers to the image to fix the vulnerabilities. 198 | 199 | In this section, you'll use Copacetic to patch the `azure-voting-app-rust` container image for vulnerabilities. 200 | 201 | ### Build and push the container images 202 | 203 | A currently limitation of Copacetic is that it can only patch container images that are stored in a remote container registry. So, you'll need to push the `azure-voting-app-rust` container image to the Azure Container Registry. 204 | 205 | Run the following commands to build the `azure-voting-app-rust` container image and push it to the Azure Container Registry: 206 | 207 | ```bash 208 | ACR_IMAGE=$ACR_NAME.azurecr.io/azure-voting-app-rust:v0.1-alpha; 209 | docker tag $IMAGE $ACR_IMAGE; 210 | docker push $ACR_IMAGE 211 | ``` 212 | 213 |
214 | 215 | > If you encounter an authentication error, run `az acr login --name $ACR_NAME` command to authenticate to the Azure Container Registry then try the `docker push` command again. 216 | 217 |
218 | 219 | ### Patching the container image 220 | 221 | Now that you have your container images built and pushed to the Azure Container Registry, let's proceed with patching them. 222 | 223 | First, start the Copacetic buildkit daemon by running the following command: 224 | 225 | ```bash 226 | sudo ./bin/buildkitd &> /dev/null & 227 | ``` 228 | 229 | Next, run `copa` to patch the `azure-voting-app-rust` container image: 230 | 231 | ```bash 232 | sudo copa patch -i ${ACR_IMAGE} -r ./patch.json -t v0.1-alpha-1 233 | ``` 234 | 235 |
236 | 237 | > Appending a hyphen followed by a number is a semantic version compliant way to indicate how many times a container image has been patched. 238 | 239 | 240 |
241 | 242 | Once the patching process is complete, there will be a newly patched container image on your local machine. You can view the patched image by running the following command: 243 | 244 | ```bash 245 | docker images 246 | ``` 247 | 248 | To confirm that Copacetic patched the container image, rerun the `trivy image` command on the patched image: 249 | 250 | ```bash 251 | ACR_IMAGE_PATCHED=${ACR_NAME}.azurecr.io/azure-voting-app-rust:v0.1-alpha-1; 252 | trivy image --severity CRITICAL --scanners vuln ${ACR_IMAGE_PATCHED} 253 | ``` 254 | 255 |
256 | Example Output 257 | 258 | ```output 259 | 2023-07-14T17:45:27.463Z INFO Vulnerability scanning is enabled 260 | 2023-07-14T17:45:30.157Z INFO Detected OS: debian 261 | 2023-07-14T17:45:30.157Z INFO Detecting Debian vulnerabilities... 262 | 2023-07-14T17:45:30.165Z INFO Number of language-specific files: 0 263 | 264 | s3cexampleacr.azurecr.io/azure-voting-app-rust:v0.1-alpha 265 | 266 | Total: 1 (CRITICAL: 1) 267 | ``` 268 | 269 |
270 | 271 | Lastly, push the patched image tag to the Azure Container Registry: 272 | 273 | ```bash 274 | docker push ${ACR_IMAGE_PATCHED} 275 | ``` 276 | 277 | ### Tag and push the Postgres image to ACR 278 | 279 | You just pushed a patched version of the `azure-voting-app-rust` image to Azure Container Registry. But the app won't run witout a valid database container. So, next you'll tag and push a version of the Postgres SQL container to your ACR instance. 280 | 281 | ```bash 282 | docker tag postgres:15.0-alpine $ACR_NAME.azurecr.io/postgres:15.0-alpine 283 | docker push $ACR_NAME.azurecr.io/postgres:15.0-alpine 284 | ``` 285 | 286 | --- 287 | 288 | ## Signing container images with Notation 289 | 290 | In this section, you'll learn how to use Notation, a command-line tool from the CNCF Notary project, to sign and verify container images, which adds an extra layer of security to your deployment pipeline. 291 | 292 | ### Why Sign Container Images? 293 | 294 | On the surface, signing container images just seems like more work. But, signing container images is crucial for several reasons: 295 | 296 | * **Image Integrity**: Signing verifies that the image hasn't been tampered with or altered since the signature was applied. It ensures that the image you deploy is the exact image you intended to use. 297 | * **Authentication**: Signature validation confirms the authenticity of the image, providing a level of trust in the source. 298 | * **Security and Compliance**: By ensuring image integrity and authenticity, signing helps meet security and compliance requirements, especially in regulated industries. 299 | 300 | ### Adding a key to Notary 301 | 302 | To sign container images with Notation, you need to add a key to the Notary configuration. As part of your infrastructure setup, you created a certificate and stored it in Azure Key Vault. You'll use the KEY_ID of that certificate to identify the certificate used for the digital signatures of your container images. 303 | 304 | To get the `KEY_ID` from Azure Key Vault, run the following command: 305 | 306 | ```bash 307 | KEY_ID=$(az keyvault certificate show --name $CERT_NAME --vault-name $AKV_NAME --query kid -o tsv) 308 | ``` 309 | 310 | Next, add the certificate to Notary using the notation key add command: 311 | 312 | ```bash 313 | notation key add --plugin azure-kv default --id $KEY_ID --default 314 | ``` 315 | 316 | You can verify that the key was added successfully by running: 317 | 318 | ```bash 319 | notation key list 320 | ``` 321 | 322 | ### Getting the Docker Image Digest 323 | 324 | Signing a container image solely by its tag poses a significant security risk due to the mutability of tags, meaning they can be changed over time. When you sign an image by its tag, there is no way to guarantee that the tag you signed hasn't been modified or updated to point to a different version of the image. 325 | 326 | To mitigate this risk, it is recommended to sign container images using their Docker image digest, which is a unique identifier generated by the SHA256 cryptographic hash function. Docker image digests are immutable and remain constant as long as the image content remains unchanged. This immutability ensures that the signed content cannot be tampered with, providing a reliable way to reference a specific version of the image. 327 | 328 | By using digests (SHA256) for signing, an additional layer of security is added, ensuring the integrity and authenticity of container images. It significantly reduces the vulnerability to tampering or exploitation, making them more secure for use in production environments. 329 | 330 | To get the digests of the container images, run the following commands: 331 | 332 | ```bash 333 | APP_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${ACR_IMAGE_PATCHED}) 334 | 335 | DB_DIGEST=$(docker inspect --format='{{index .RepoDigests 1}}' $ACR_NAME.azurecr.io/postgres:15.0-alpine) 336 | ``` 337 | 338 |
339 | 340 | > **Tip**: If you want to always get the most recent RepoDigest, you can use the following command-line trickery: 341 | 342 | ```bash 343 | docker image inspect --format='{{range $digest := .RepoDigests}}{{println $digest}}{{end}}' | sort | tail -1 344 | ``` 345 | 346 |
347 | 348 | ### Signing the container image with Notation 349 | 350 | With the key added to Notary, you can now sign your container images. Let's sign the azure-voting-app-rust and postgres:15.0-alpine images pushed to your Azure Container Registry. 351 | 352 | To sign the images, use the following commands: 353 | 354 | ```bash 355 | notation sign $APP_DIGEST; 356 | notation sign $DB_DIGEST 357 | ``` 358 | 359 | These commands will apply signatures to the specified images, ensuring their integrity and authenticity. 360 | 361 | --- 362 | 363 | ## Preventing unsigned container images from being deployed with Ratify 364 | 365 | In this section, you'll learn how to use Ratify to prevent unsigned container images from being deployed to your AKS cluster. 366 | 367 | ### Deploy Unsigned App Images 368 | 369 | The first step is to deploy the app images without any signing. 370 | 371 | Use the following command to apply the manifests: 372 | 373 | ```bash 374 | kubectl apply -f manifests/ 375 | ``` 376 | 377 | Next, check the Ratify logs for the blocked deployment: 378 | 379 | ```bash 380 | kubectl logs -n gatekeeper-system deployment/ratify 381 | ``` 382 | 383 |
384 | Example Output 385 | 386 | ```output 387 | time="2023-07-26T17:42:33Z" level=error msg="failed to mutate image reference azure-voting-app-rust:v0.1-alpha: HEAD \"https://registry-1.docker.io/v2/library/azure-voting-app-rust/manifests/v0.1-alpha\": response status code 401: Unauthorized" 388 | time="2023-07-26T17:42:33Z" level=warning msg="failed to resolve the subject descriptor from store oras with error HEAD \"https://registry-1.docker.io/v2/library/azure-voting-app-rust/manifests/v0.1-alpha\": response status code 401: Unauthorized\n" 389 | ``` 390 | 391 |
392 | 393 | Clean up the failed deployment by running the following command: 394 | 395 | ```bash 396 | kubectl delete -f manifests/ 397 | ``` 398 | 399 | ### Deploy Signed App Images 400 | 401 | Now that you've seen how Ratify prevents unsigned images from being deployed, let's deploy the signed images. 402 | 403 | Use the following command to update the manifests with the signed image tags: 404 | 405 | 406 | ```bash 407 | sed -i "s|azure-voting-app-rust:v0.1-alpha|${APP_DIGEST}|" ./manifests/deployment-app.yaml 408 | 409 | sed -i "s|postgres:15.0-alpine|${DB_DIGEST}|g" manifests/postgres.yaml 410 | ``` 411 | 412 | Next, apply the manifests: 413 | 414 | ```bash 415 | kubectl apply -f manifests/ 416 | ``` 417 | 418 | Lastly, check the pods to confirm that the app is running: 419 | 420 | ```bash 421 | kubectl get pods 422 | ``` 423 | 424 |
425 | 426 | Example Output 427 | 428 | ```output 429 | NAME READY STATUS RESTARTS AGE 430 | azure-voting-app-7fb9f67f6-zl64t 1/1 Running 0 17s 431 | azure-voting-db-699fcf6bcd-g4nhz 1/1 Running 0 17s 432 | ``` 433 | 434 |
435 | 436 | To understand why Ratify allowed the signed images to be deployed you can take a look at the constraints and templates that were applied to the cluster. 437 | 438 | ```bash 439 | code manifests/constraints.yaml 440 | code manifests/templates.yaml 441 | ``` 442 | 443 | After a few minutes, check the status of the ingress resource to get the public IP address of the app: 444 | 445 | ```bash 446 | kubectl get ingress 447 | ``` 448 | 449 |
450 | 451 | Example Output 452 | 453 | ```output 454 | NAME CLASS HOSTS ADDRESS PORTS AGE 455 | azure-voting-app webapprouting.kubernetes.azure.com * 4.236.203.158 80 60s 456 | ``` 457 | 458 |
459 | 460 | Open a browser and navigate to the IP address of the app. You should see the Azure Voting App. 461 | 462 | --- 463 | 464 | References: 465 | - [Build, sign, and verify container images using Notary and Azure Key Vault](https://learn.microsoft.com/azure/container-registry/container-registry-tutorial-sign-build-push) 466 | - [Ratify on Azure: Allow only signed images to be deployed on AKS with Notation and Ratify](https://github.com/deislabs/ratify/blob/main/docs/quickstarts/ratify-on-azure.md) -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "actix-codec" 7 | version = "0.5.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" 10 | dependencies = [ 11 | "bitflags", 12 | "bytes", 13 | "futures-core", 14 | "futures-sink", 15 | "log", 16 | "memchr", 17 | "pin-project-lite", 18 | "tokio", 19 | "tokio-util", 20 | ] 21 | 22 | [[package]] 23 | name = "actix-files" 24 | version = "0.6.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" 27 | dependencies = [ 28 | "actix-http", 29 | "actix-service", 30 | "actix-utils", 31 | "actix-web", 32 | "askama_escape", 33 | "bitflags", 34 | "bytes", 35 | "derive_more", 36 | "futures-core", 37 | "http-range", 38 | "log", 39 | "mime", 40 | "mime_guess", 41 | "percent-encoding", 42 | "pin-project-lite", 43 | ] 44 | 45 | [[package]] 46 | name = "actix-http" 47 | version = "3.2.2" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "0c83abf9903e1f0ad9973cc4f7b9767fd5a03a583f51a5b7a339e07987cd2724" 50 | dependencies = [ 51 | "actix-codec", 52 | "actix-rt", 53 | "actix-service", 54 | "actix-utils", 55 | "ahash", 56 | "base64", 57 | "bitflags", 58 | "brotli", 59 | "bytes", 60 | "bytestring", 61 | "derive_more", 62 | "encoding_rs", 63 | "flate2", 64 | "futures-core", 65 | "h2", 66 | "http", 67 | "httparse", 68 | "httpdate", 69 | "itoa", 70 | "language-tags", 71 | "local-channel", 72 | "mime", 73 | "percent-encoding", 74 | "pin-project-lite", 75 | "rand", 76 | "sha1", 77 | "smallvec", 78 | "tracing", 79 | "zstd", 80 | ] 81 | 82 | [[package]] 83 | name = "actix-macros" 84 | version = "0.2.3" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" 87 | dependencies = [ 88 | "quote", 89 | "syn", 90 | ] 91 | 92 | [[package]] 93 | name = "actix-router" 94 | version = "0.5.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" 97 | dependencies = [ 98 | "bytestring", 99 | "http", 100 | "regex", 101 | "serde", 102 | "tracing", 103 | ] 104 | 105 | [[package]] 106 | name = "actix-rt" 107 | version = "2.7.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" 110 | dependencies = [ 111 | "futures-core", 112 | "tokio", 113 | ] 114 | 115 | [[package]] 116 | name = "actix-server" 117 | version = "2.1.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" 120 | dependencies = [ 121 | "actix-rt", 122 | "actix-service", 123 | "actix-utils", 124 | "futures-core", 125 | "futures-util", 126 | "mio", 127 | "num_cpus", 128 | "socket2", 129 | "tokio", 130 | "tracing", 131 | ] 132 | 133 | [[package]] 134 | name = "actix-service" 135 | version = "2.0.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" 138 | dependencies = [ 139 | "futures-core", 140 | "paste", 141 | "pin-project-lite", 142 | ] 143 | 144 | [[package]] 145 | name = "actix-utils" 146 | version = "3.0.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" 149 | dependencies = [ 150 | "local-waker", 151 | "pin-project-lite", 152 | ] 153 | 154 | [[package]] 155 | name = "actix-web" 156 | version = "4.2.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "d48f7b6534e06c7bfc72ee91db7917d4af6afe23e7d223b51e68fffbb21e96b9" 159 | dependencies = [ 160 | "actix-codec", 161 | "actix-http", 162 | "actix-macros", 163 | "actix-router", 164 | "actix-rt", 165 | "actix-server", 166 | "actix-service", 167 | "actix-utils", 168 | "actix-web-codegen", 169 | "ahash", 170 | "bytes", 171 | "bytestring", 172 | "cfg-if", 173 | "cookie", 174 | "derive_more", 175 | "encoding_rs", 176 | "futures-core", 177 | "futures-util", 178 | "http", 179 | "itoa", 180 | "language-tags", 181 | "log", 182 | "mime", 183 | "once_cell", 184 | "pin-project-lite", 185 | "regex", 186 | "serde", 187 | "serde_json", 188 | "serde_urlencoded", 189 | "smallvec", 190 | "socket2", 191 | "time", 192 | "url", 193 | ] 194 | 195 | [[package]] 196 | name = "actix-web-codegen" 197 | version = "4.1.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" 200 | dependencies = [ 201 | "actix-router", 202 | "proc-macro2", 203 | "quote", 204 | "syn", 205 | ] 206 | 207 | [[package]] 208 | name = "adler" 209 | version = "1.0.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 212 | 213 | [[package]] 214 | name = "ahash" 215 | version = "0.7.6" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 218 | dependencies = [ 219 | "getrandom", 220 | "once_cell", 221 | "version_check", 222 | ] 223 | 224 | [[package]] 225 | name = "aho-corasick" 226 | version = "0.7.20" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 229 | dependencies = [ 230 | "memchr", 231 | ] 232 | 233 | [[package]] 234 | name = "alloc-no-stdlib" 235 | version = "2.0.4" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 238 | 239 | [[package]] 240 | name = "alloc-stdlib" 241 | version = "0.2.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 244 | dependencies = [ 245 | "alloc-no-stdlib", 246 | ] 247 | 248 | [[package]] 249 | name = "askama_escape" 250 | version = "0.10.3" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" 253 | 254 | [[package]] 255 | name = "autocfg" 256 | version = "1.1.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 259 | 260 | [[package]] 261 | name = "azure-voting-app-rust" 262 | version = "0.1.0" 263 | dependencies = [ 264 | "actix-files", 265 | "actix-web", 266 | "diesel", 267 | "diesel_migrations", 268 | "dotenvy", 269 | "env_logger", 270 | "handlebars", 271 | "lazy_static", 272 | "log", 273 | "r2d2", 274 | "serde", 275 | "serde_json", 276 | ] 277 | 278 | [[package]] 279 | name = "base64" 280 | version = "0.13.1" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 283 | 284 | [[package]] 285 | name = "bitflags" 286 | version = "1.3.2" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 289 | 290 | [[package]] 291 | name = "block-buffer" 292 | version = "0.10.3" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" 295 | dependencies = [ 296 | "generic-array", 297 | ] 298 | 299 | [[package]] 300 | name = "brotli" 301 | version = "3.3.4" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" 304 | dependencies = [ 305 | "alloc-no-stdlib", 306 | "alloc-stdlib", 307 | "brotli-decompressor", 308 | ] 309 | 310 | [[package]] 311 | name = "brotli-decompressor" 312 | version = "2.3.2" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" 315 | dependencies = [ 316 | "alloc-no-stdlib", 317 | "alloc-stdlib", 318 | ] 319 | 320 | [[package]] 321 | name = "byteorder" 322 | version = "1.4.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 325 | 326 | [[package]] 327 | name = "bytes" 328 | version = "1.3.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 331 | 332 | [[package]] 333 | name = "bytestring" 334 | version = "1.2.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1" 337 | dependencies = [ 338 | "bytes", 339 | ] 340 | 341 | [[package]] 342 | name = "cc" 343 | version = "1.0.78" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" 346 | dependencies = [ 347 | "jobserver", 348 | ] 349 | 350 | [[package]] 351 | name = "cfg-if" 352 | version = "1.0.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 355 | 356 | [[package]] 357 | name = "convert_case" 358 | version = "0.4.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 361 | 362 | [[package]] 363 | name = "cookie" 364 | version = "0.16.2" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" 367 | dependencies = [ 368 | "percent-encoding", 369 | "time", 370 | "version_check", 371 | ] 372 | 373 | [[package]] 374 | name = "cpufeatures" 375 | version = "0.2.5" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" 378 | dependencies = [ 379 | "libc", 380 | ] 381 | 382 | [[package]] 383 | name = "crc32fast" 384 | version = "1.3.2" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 387 | dependencies = [ 388 | "cfg-if", 389 | ] 390 | 391 | [[package]] 392 | name = "crypto-common" 393 | version = "0.1.6" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 396 | dependencies = [ 397 | "generic-array", 398 | "typenum", 399 | ] 400 | 401 | [[package]] 402 | name = "derive_more" 403 | version = "0.99.17" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 406 | dependencies = [ 407 | "convert_case", 408 | "proc-macro2", 409 | "quote", 410 | "rustc_version", 411 | "syn", 412 | ] 413 | 414 | [[package]] 415 | name = "diesel" 416 | version = "2.0.2" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "68c186a7418a2aac330bb76cde82f16c36b03a66fb91db32d20214311f9f6545" 419 | dependencies = [ 420 | "bitflags", 421 | "byteorder", 422 | "diesel_derives", 423 | "itoa", 424 | "pq-sys", 425 | "r2d2", 426 | ] 427 | 428 | [[package]] 429 | name = "diesel_derives" 430 | version = "2.0.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "143b758c91dbc3fe1fdcb0dba5bd13276c6a66422f2ef5795b58488248a310aa" 433 | dependencies = [ 434 | "proc-macro-error", 435 | "proc-macro2", 436 | "quote", 437 | "syn", 438 | ] 439 | 440 | [[package]] 441 | name = "diesel_migrations" 442 | version = "2.0.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2" 445 | dependencies = [ 446 | "diesel", 447 | "migrations_internals", 448 | "migrations_macros", 449 | ] 450 | 451 | [[package]] 452 | name = "digest" 453 | version = "0.10.6" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 456 | dependencies = [ 457 | "block-buffer", 458 | "crypto-common", 459 | ] 460 | 461 | [[package]] 462 | name = "dotenvy" 463 | version = "0.15.6" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" 466 | 467 | [[package]] 468 | name = "encoding_rs" 469 | version = "0.8.31" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 472 | dependencies = [ 473 | "cfg-if", 474 | ] 475 | 476 | [[package]] 477 | name = "env_logger" 478 | version = "0.10.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 481 | dependencies = [ 482 | "humantime", 483 | "is-terminal", 484 | "log", 485 | "regex", 486 | "termcolor", 487 | ] 488 | 489 | [[package]] 490 | name = "errno" 491 | version = "0.2.8" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 494 | dependencies = [ 495 | "errno-dragonfly", 496 | "libc", 497 | "winapi", 498 | ] 499 | 500 | [[package]] 501 | name = "errno-dragonfly" 502 | version = "0.1.2" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 505 | dependencies = [ 506 | "cc", 507 | "libc", 508 | ] 509 | 510 | [[package]] 511 | name = "flate2" 512 | version = "1.0.25" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 515 | dependencies = [ 516 | "crc32fast", 517 | "miniz_oxide", 518 | ] 519 | 520 | [[package]] 521 | name = "fnv" 522 | version = "1.0.7" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 525 | 526 | [[package]] 527 | name = "form_urlencoded" 528 | version = "1.1.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 531 | dependencies = [ 532 | "percent-encoding", 533 | ] 534 | 535 | [[package]] 536 | name = "futures-core" 537 | version = "0.3.25" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" 540 | 541 | [[package]] 542 | name = "futures-sink" 543 | version = "0.3.25" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" 546 | 547 | [[package]] 548 | name = "futures-task" 549 | version = "0.3.25" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" 552 | 553 | [[package]] 554 | name = "futures-util" 555 | version = "0.3.25" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" 558 | dependencies = [ 559 | "futures-core", 560 | "futures-task", 561 | "pin-project-lite", 562 | "pin-utils", 563 | ] 564 | 565 | [[package]] 566 | name = "generic-array" 567 | version = "0.14.6" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 570 | dependencies = [ 571 | "typenum", 572 | "version_check", 573 | ] 574 | 575 | [[package]] 576 | name = "getrandom" 577 | version = "0.2.8" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 580 | dependencies = [ 581 | "cfg-if", 582 | "libc", 583 | "wasi", 584 | ] 585 | 586 | [[package]] 587 | name = "h2" 588 | version = "0.3.15" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" 591 | dependencies = [ 592 | "bytes", 593 | "fnv", 594 | "futures-core", 595 | "futures-sink", 596 | "futures-util", 597 | "http", 598 | "indexmap", 599 | "slab", 600 | "tokio", 601 | "tokio-util", 602 | "tracing", 603 | ] 604 | 605 | [[package]] 606 | name = "handlebars" 607 | version = "4.3.6" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" 610 | dependencies = [ 611 | "log", 612 | "pest", 613 | "pest_derive", 614 | "serde", 615 | "serde_json", 616 | "thiserror", 617 | "walkdir", 618 | ] 619 | 620 | [[package]] 621 | name = "hashbrown" 622 | version = "0.12.3" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 625 | 626 | [[package]] 627 | name = "hermit-abi" 628 | version = "0.2.6" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 631 | dependencies = [ 632 | "libc", 633 | ] 634 | 635 | [[package]] 636 | name = "http" 637 | version = "0.2.8" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 640 | dependencies = [ 641 | "bytes", 642 | "fnv", 643 | "itoa", 644 | ] 645 | 646 | [[package]] 647 | name = "http-range" 648 | version = "0.1.5" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" 651 | 652 | [[package]] 653 | name = "httparse" 654 | version = "1.8.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 657 | 658 | [[package]] 659 | name = "httpdate" 660 | version = "1.0.2" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 663 | 664 | [[package]] 665 | name = "humantime" 666 | version = "2.1.0" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 669 | 670 | [[package]] 671 | name = "idna" 672 | version = "0.3.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 675 | dependencies = [ 676 | "unicode-bidi", 677 | "unicode-normalization", 678 | ] 679 | 680 | [[package]] 681 | name = "indexmap" 682 | version = "1.9.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 685 | dependencies = [ 686 | "autocfg", 687 | "hashbrown", 688 | ] 689 | 690 | [[package]] 691 | name = "io-lifetimes" 692 | version = "1.0.3" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 695 | dependencies = [ 696 | "libc", 697 | "windows-sys", 698 | ] 699 | 700 | [[package]] 701 | name = "is-terminal" 702 | version = "0.4.2" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" 705 | dependencies = [ 706 | "hermit-abi", 707 | "io-lifetimes", 708 | "rustix", 709 | "windows-sys", 710 | ] 711 | 712 | [[package]] 713 | name = "itoa" 714 | version = "1.0.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 717 | 718 | [[package]] 719 | name = "jobserver" 720 | version = "0.1.25" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" 723 | dependencies = [ 724 | "libc", 725 | ] 726 | 727 | [[package]] 728 | name = "language-tags" 729 | version = "0.3.2" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" 732 | 733 | [[package]] 734 | name = "lazy_static" 735 | version = "1.4.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 738 | 739 | [[package]] 740 | name = "libc" 741 | version = "0.2.138" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 744 | 745 | [[package]] 746 | name = "linux-raw-sys" 747 | version = "0.1.4" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 750 | 751 | [[package]] 752 | name = "local-channel" 753 | version = "0.1.3" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" 756 | dependencies = [ 757 | "futures-core", 758 | "futures-sink", 759 | "futures-util", 760 | "local-waker", 761 | ] 762 | 763 | [[package]] 764 | name = "local-waker" 765 | version = "0.1.3" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" 768 | 769 | [[package]] 770 | name = "lock_api" 771 | version = "0.4.9" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 774 | dependencies = [ 775 | "autocfg", 776 | "scopeguard", 777 | ] 778 | 779 | [[package]] 780 | name = "log" 781 | version = "0.4.17" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 784 | dependencies = [ 785 | "cfg-if", 786 | ] 787 | 788 | [[package]] 789 | name = "memchr" 790 | version = "2.5.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 793 | 794 | [[package]] 795 | name = "migrations_internals" 796 | version = "2.0.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc" 799 | dependencies = [ 800 | "serde", 801 | "toml", 802 | ] 803 | 804 | [[package]] 805 | name = "migrations_macros" 806 | version = "2.0.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58" 809 | dependencies = [ 810 | "migrations_internals", 811 | "proc-macro2", 812 | "quote", 813 | ] 814 | 815 | [[package]] 816 | name = "mime" 817 | version = "0.3.16" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 820 | 821 | [[package]] 822 | name = "mime_guess" 823 | version = "2.0.4" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 826 | dependencies = [ 827 | "mime", 828 | "unicase", 829 | ] 830 | 831 | [[package]] 832 | name = "miniz_oxide" 833 | version = "0.6.2" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 836 | dependencies = [ 837 | "adler", 838 | ] 839 | 840 | [[package]] 841 | name = "mio" 842 | version = "0.8.5" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 845 | dependencies = [ 846 | "libc", 847 | "log", 848 | "wasi", 849 | "windows-sys", 850 | ] 851 | 852 | [[package]] 853 | name = "num_cpus" 854 | version = "1.15.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 857 | dependencies = [ 858 | "hermit-abi", 859 | "libc", 860 | ] 861 | 862 | [[package]] 863 | name = "once_cell" 864 | version = "1.16.0" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" 867 | 868 | [[package]] 869 | name = "parking_lot" 870 | version = "0.12.1" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 873 | dependencies = [ 874 | "lock_api", 875 | "parking_lot_core", 876 | ] 877 | 878 | [[package]] 879 | name = "parking_lot_core" 880 | version = "0.9.5" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" 883 | dependencies = [ 884 | "cfg-if", 885 | "libc", 886 | "redox_syscall", 887 | "smallvec", 888 | "windows-sys", 889 | ] 890 | 891 | [[package]] 892 | name = "paste" 893 | version = "1.0.11" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" 896 | 897 | [[package]] 898 | name = "percent-encoding" 899 | version = "2.2.0" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 902 | 903 | [[package]] 904 | name = "pest" 905 | version = "2.5.1" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" 908 | dependencies = [ 909 | "thiserror", 910 | "ucd-trie", 911 | ] 912 | 913 | [[package]] 914 | name = "pest_derive" 915 | version = "2.5.1" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344" 918 | dependencies = [ 919 | "pest", 920 | "pest_generator", 921 | ] 922 | 923 | [[package]] 924 | name = "pest_generator" 925 | version = "2.5.1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c" 928 | dependencies = [ 929 | "pest", 930 | "pest_meta", 931 | "proc-macro2", 932 | "quote", 933 | "syn", 934 | ] 935 | 936 | [[package]] 937 | name = "pest_meta" 938 | version = "2.5.1" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20" 941 | dependencies = [ 942 | "once_cell", 943 | "pest", 944 | "sha1", 945 | ] 946 | 947 | [[package]] 948 | name = "pin-project-lite" 949 | version = "0.2.9" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 952 | 953 | [[package]] 954 | name = "pin-utils" 955 | version = "0.1.0" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 958 | 959 | [[package]] 960 | name = "ppv-lite86" 961 | version = "0.2.17" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 964 | 965 | [[package]] 966 | name = "pq-sys" 967 | version = "0.4.7" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "3b845d6d8ec554f972a2c5298aad68953fd64e7441e846075450b44656a016d1" 970 | dependencies = [ 971 | "vcpkg", 972 | ] 973 | 974 | [[package]] 975 | name = "proc-macro-error" 976 | version = "1.0.4" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 979 | dependencies = [ 980 | "proc-macro-error-attr", 981 | "proc-macro2", 982 | "quote", 983 | "syn", 984 | "version_check", 985 | ] 986 | 987 | [[package]] 988 | name = "proc-macro-error-attr" 989 | version = "1.0.4" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 992 | dependencies = [ 993 | "proc-macro2", 994 | "quote", 995 | "version_check", 996 | ] 997 | 998 | [[package]] 999 | name = "proc-macro2" 1000 | version = "1.0.49" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" 1003 | dependencies = [ 1004 | "unicode-ident", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "quote" 1009 | version = "1.0.23" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 1012 | dependencies = [ 1013 | "proc-macro2", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "r2d2" 1018 | version = "0.8.10" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" 1021 | dependencies = [ 1022 | "log", 1023 | "parking_lot", 1024 | "scheduled-thread-pool", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "rand" 1029 | version = "0.8.5" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1032 | dependencies = [ 1033 | "libc", 1034 | "rand_chacha", 1035 | "rand_core", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "rand_chacha" 1040 | version = "0.3.1" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1043 | dependencies = [ 1044 | "ppv-lite86", 1045 | "rand_core", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "rand_core" 1050 | version = "0.6.4" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1053 | dependencies = [ 1054 | "getrandom", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "redox_syscall" 1059 | version = "0.2.16" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1062 | dependencies = [ 1063 | "bitflags", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "regex" 1068 | version = "1.7.0" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 1071 | dependencies = [ 1072 | "aho-corasick", 1073 | "memchr", 1074 | "regex-syntax", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "regex-syntax" 1079 | version = "0.6.28" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 1082 | 1083 | [[package]] 1084 | name = "rustc_version" 1085 | version = "0.4.0" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1088 | dependencies = [ 1089 | "semver", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "rustix" 1094 | version = "0.36.5" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" 1097 | dependencies = [ 1098 | "bitflags", 1099 | "errno", 1100 | "io-lifetimes", 1101 | "libc", 1102 | "linux-raw-sys", 1103 | "windows-sys", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "ryu" 1108 | version = "1.0.12" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 1111 | 1112 | [[package]] 1113 | name = "same-file" 1114 | version = "1.0.6" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1117 | dependencies = [ 1118 | "winapi-util", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "scheduled-thread-pool" 1123 | version = "0.2.6" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf" 1126 | dependencies = [ 1127 | "parking_lot", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "scopeguard" 1132 | version = "1.1.0" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1135 | 1136 | [[package]] 1137 | name = "semver" 1138 | version = "1.0.16" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" 1141 | 1142 | [[package]] 1143 | name = "serde" 1144 | version = "1.0.151" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" 1147 | dependencies = [ 1148 | "serde_derive", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "serde_derive" 1153 | version = "1.0.151" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" 1156 | dependencies = [ 1157 | "proc-macro2", 1158 | "quote", 1159 | "syn", 1160 | ] 1161 | 1162 | [[package]] 1163 | name = "serde_json" 1164 | version = "1.0.91" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 1167 | dependencies = [ 1168 | "itoa", 1169 | "ryu", 1170 | "serde", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "serde_urlencoded" 1175 | version = "0.7.1" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1178 | dependencies = [ 1179 | "form_urlencoded", 1180 | "itoa", 1181 | "ryu", 1182 | "serde", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "sha1" 1187 | version = "0.10.5" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 1190 | dependencies = [ 1191 | "cfg-if", 1192 | "cpufeatures", 1193 | "digest", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "signal-hook-registry" 1198 | version = "1.4.0" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1201 | dependencies = [ 1202 | "libc", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "slab" 1207 | version = "0.4.7" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1210 | dependencies = [ 1211 | "autocfg", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "smallvec" 1216 | version = "1.10.0" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 1219 | 1220 | [[package]] 1221 | name = "socket2" 1222 | version = "0.4.7" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" 1225 | dependencies = [ 1226 | "libc", 1227 | "winapi", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "syn" 1232 | version = "1.0.107" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 1235 | dependencies = [ 1236 | "proc-macro2", 1237 | "quote", 1238 | "unicode-ident", 1239 | ] 1240 | 1241 | [[package]] 1242 | name = "termcolor" 1243 | version = "1.1.3" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1246 | dependencies = [ 1247 | "winapi-util", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "thiserror" 1252 | version = "1.0.38" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 1255 | dependencies = [ 1256 | "thiserror-impl", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "thiserror-impl" 1261 | version = "1.0.38" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 1264 | dependencies = [ 1265 | "proc-macro2", 1266 | "quote", 1267 | "syn", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "time" 1272 | version = "0.3.17" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" 1275 | dependencies = [ 1276 | "itoa", 1277 | "serde", 1278 | "time-core", 1279 | "time-macros", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "time-core" 1284 | version = "0.1.0" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 1287 | 1288 | [[package]] 1289 | name = "time-macros" 1290 | version = "0.2.6" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" 1293 | dependencies = [ 1294 | "time-core", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "tinyvec" 1299 | version = "1.6.0" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1302 | dependencies = [ 1303 | "tinyvec_macros", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "tinyvec_macros" 1308 | version = "0.1.0" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1311 | 1312 | [[package]] 1313 | name = "tokio" 1314 | version = "1.24.2" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" 1317 | dependencies = [ 1318 | "autocfg", 1319 | "bytes", 1320 | "libc", 1321 | "memchr", 1322 | "mio", 1323 | "parking_lot", 1324 | "pin-project-lite", 1325 | "signal-hook-registry", 1326 | "socket2", 1327 | "windows-sys", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "tokio-util" 1332 | version = "0.7.4" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1335 | dependencies = [ 1336 | "bytes", 1337 | "futures-core", 1338 | "futures-sink", 1339 | "pin-project-lite", 1340 | "tokio", 1341 | "tracing", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "toml" 1346 | version = "0.5.10" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" 1349 | dependencies = [ 1350 | "serde", 1351 | ] 1352 | 1353 | [[package]] 1354 | name = "tracing" 1355 | version = "0.1.37" 1356 | source = "registry+https://github.com/rust-lang/crates.io-index" 1357 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1358 | dependencies = [ 1359 | "cfg-if", 1360 | "log", 1361 | "pin-project-lite", 1362 | "tracing-core", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "tracing-core" 1367 | version = "0.1.30" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1370 | dependencies = [ 1371 | "once_cell", 1372 | ] 1373 | 1374 | [[package]] 1375 | name = "typenum" 1376 | version = "1.16.0" 1377 | source = "registry+https://github.com/rust-lang/crates.io-index" 1378 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1379 | 1380 | [[package]] 1381 | name = "ucd-trie" 1382 | version = "0.1.5" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" 1385 | 1386 | [[package]] 1387 | name = "unicase" 1388 | version = "2.6.0" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1391 | dependencies = [ 1392 | "version_check", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "unicode-bidi" 1397 | version = "0.3.8" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1400 | 1401 | [[package]] 1402 | name = "unicode-ident" 1403 | version = "1.0.6" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1406 | 1407 | [[package]] 1408 | name = "unicode-normalization" 1409 | version = "0.1.22" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1412 | dependencies = [ 1413 | "tinyvec", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "url" 1418 | version = "2.3.1" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1421 | dependencies = [ 1422 | "form_urlencoded", 1423 | "idna", 1424 | "percent-encoding", 1425 | ] 1426 | 1427 | [[package]] 1428 | name = "vcpkg" 1429 | version = "0.2.15" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1432 | 1433 | [[package]] 1434 | name = "version_check" 1435 | version = "0.9.4" 1436 | source = "registry+https://github.com/rust-lang/crates.io-index" 1437 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1438 | 1439 | [[package]] 1440 | name = "walkdir" 1441 | version = "2.3.2" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 1444 | dependencies = [ 1445 | "same-file", 1446 | "winapi", 1447 | "winapi-util", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "wasi" 1452 | version = "0.11.0+wasi-snapshot-preview1" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1455 | 1456 | [[package]] 1457 | name = "winapi" 1458 | version = "0.3.9" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1461 | dependencies = [ 1462 | "winapi-i686-pc-windows-gnu", 1463 | "winapi-x86_64-pc-windows-gnu", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "winapi-i686-pc-windows-gnu" 1468 | version = "0.4.0" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1471 | 1472 | [[package]] 1473 | name = "winapi-util" 1474 | version = "0.1.5" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1477 | dependencies = [ 1478 | "winapi", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "winapi-x86_64-pc-windows-gnu" 1483 | version = "0.4.0" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1486 | 1487 | [[package]] 1488 | name = "windows-sys" 1489 | version = "0.42.0" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1492 | dependencies = [ 1493 | "windows_aarch64_gnullvm", 1494 | "windows_aarch64_msvc", 1495 | "windows_i686_gnu", 1496 | "windows_i686_msvc", 1497 | "windows_x86_64_gnu", 1498 | "windows_x86_64_gnullvm", 1499 | "windows_x86_64_msvc", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "windows_aarch64_gnullvm" 1504 | version = "0.42.0" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" 1507 | 1508 | [[package]] 1509 | name = "windows_aarch64_msvc" 1510 | version = "0.42.0" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" 1513 | 1514 | [[package]] 1515 | name = "windows_i686_gnu" 1516 | version = "0.42.0" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" 1519 | 1520 | [[package]] 1521 | name = "windows_i686_msvc" 1522 | version = "0.42.0" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" 1525 | 1526 | [[package]] 1527 | name = "windows_x86_64_gnu" 1528 | version = "0.42.0" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" 1531 | 1532 | [[package]] 1533 | name = "windows_x86_64_gnullvm" 1534 | version = "0.42.0" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" 1537 | 1538 | [[package]] 1539 | name = "windows_x86_64_msvc" 1540 | version = "0.42.0" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" 1543 | 1544 | [[package]] 1545 | name = "zstd" 1546 | version = "0.11.2+zstd.1.5.2" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" 1549 | dependencies = [ 1550 | "zstd-safe", 1551 | ] 1552 | 1553 | [[package]] 1554 | name = "zstd-safe" 1555 | version = "5.0.2+zstd.1.5.2" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" 1558 | dependencies = [ 1559 | "libc", 1560 | "zstd-sys", 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "zstd-sys" 1565 | version = "2.0.4+zstd.1.5.2" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" 1568 | dependencies = [ 1569 | "cc", 1570 | "libc", 1571 | ] 1572 | --------------------------------------------------------------------------------