Skip to content

Commit

Permalink
Add a webhook to validate trust policies (#285)
Browse files Browse the repository at this point in the history
This is based on Billy's PR, but I've rebased it on
#284 and expanded it a bunch based
on some experimentation in my dev environment.

Draft until we land the base PR.

Fixes: #247
Fixes: #46

Co-authored-by: wlynch <billy@chainguard.dev>
  • Loading branch information
mattmoor and wlynch authored May 20, 2024
1 parent c278e78 commit 8f1603b
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 7 deletions.
89 changes: 89 additions & 0 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2024 Chainguard, Inc.
// SPDX-License-Identifier: Apache-2.0

package main

import (
"context"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"time"

kms "cloud.google.com/go/kms/apiv1"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/chainguard-dev/clog"
metrics "github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics"
"github.com/kelseyhightower/envconfig"
"github.com/octo-sts/app/pkg/gcpkms"
"github.com/octo-sts/app/pkg/webhook"
)

type envConfig struct {
Port int `envconfig:"PORT" required:"true" default:"8080"`
KMSKey string `envconfig:"KMS_KEY" required:"true"`
AppID int64 `envconfig:"GITHUB_APP_ID" required:"true"`
WebhookSecret string `envconfig:"GITHUB_WEBHOOK_SECRET" required:"true"`
}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
ctx = clog.WithLogger(ctx, clog.New(slog.Default().Handler()))

go metrics.ServeMetrics()

// Setup tracing.
defer metrics.SetupTracer(ctx)()

var env envConfig
if err := envconfig.Process("", &env); err != nil {
log.Panicf("failed to process env var: %s", err)
}

kms, err := kms.NewKeyManagementClient(ctx)
if err != nil {
log.Panicf("could not create kms client: %v", err)
}
signer, err := gcpkms.New(ctx, kms, env.KMSKey)
if err != nil {
log.Panicf("error creating signer: %v", err)
}
atr, err := ghinstallation.NewAppsTransportWithOptions(http.DefaultTransport, env.AppID, ghinstallation.WithSigner(signer))
if err != nil {
log.Panicf("error creating GitHub App transport: %v", err)
}

webhookSecrets := [][]byte{}
secretmanager, err := secretmanager.NewClient(ctx)
if err != nil {
log.Panicf("could not create secret manager client: %v", err)
}
for _, name := range strings.Split(env.WebhookSecret, ",") {
resp, err := secretmanager.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
Name: name,
})
if err != nil {
log.Panicf("error fetching webhook secret %s: %v", name, err)
}
webhookSecrets = append(webhookSecrets, resp.GetPayload().GetData())
}

mux := http.NewServeMux()
mux.Handle("/", &webhook.Validator{
Transport: atr,
WebhookSecret: webhookSecrets,
})
srv := &http.Server{
Addr: fmt.Sprintf(":%d", env.Port),
ReadHeaderTimeout: 10 * time.Second,
Handler: mux,
}
log.Panic(srv.ListenAndServe())
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ require (
chainguard.dev/go-grpc-kit v0.17.3
chainguard.dev/sdk v0.1.19
cloud.google.com/go/kms v1.15.8
cloud.google.com/go/secretmanager v1.11.5
github.com/bradleyfalzon/ghinstallation/v2 v2.9.1-0.20240116154122-7838128b61c6
github.com/chainguard-dev/clog v1.3.1
github.com/chainguard-dev/terraform-infra-common v0.6.0
github.com/cloudevents/sdk-go/v2 v2.15.2
github.com/coreos/go-oidc/v3 v3.10.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-github/v58 v58.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/kelseyhightower/envconfig v1.4.0
golang.org/x/oauth2 v0.19.0
google.golang.org/api v0.174.0
google.golang.org/grpc v1.63.2
gopkg.in/yaml.v2 v2.4.0
k8s.io/apimachinery v0.29.1
sigs.k8s.io/yaml v1.4.0
)

Expand All @@ -44,6 +48,7 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs=
cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs=
cloud.google.com/go/secretmanager v1.11.5 h1:82fpF5vBBvu9XW4qj0FU2C6qVMtj1RM/XHwKXUEAfYY=
cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
Expand Down Expand Up @@ -109,6 +111,11 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c9
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -303,12 +310,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc=
k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
5 changes: 4 additions & 1 deletion iac/gclb.tf
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ module "serverless-gclb" {

public-services = {
"octo-sts.dev" = {
name = var.name
name = module.app.app.name
}
"webhook.octo-sts.dev" = {
name = module.app.webhook.name
}
}
}
18 changes: 15 additions & 3 deletions iac/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ data "google_monitoring_notification_channel" "octo-sts-slack" {
display_name = "Slack Octo STS Notification"
}

// Build each of the application images from source.
resource "ko_build" "this" {
working_dir = "${path.module}/.."
importpath = "./cmd/app"
Expand All @@ -29,6 +28,16 @@ resource "cosign_sign" "this" {
conflict = "REPLACE"
}

resource "ko_build" "webhook" {
working_dir = "${path.module}/.."
importpath = "./cmd/webhook"
}

resource "cosign_sign" "webhook" {
image = ko_build.webhook.image_ref
conflict = "REPLACE"
}

locals {
notification_channels = [
data.google_monitoring_notification_channel.octo-sts-slack.name
Expand All @@ -49,9 +58,12 @@ module "app" {
}

domain = "octo-sts.dev"
image = cosign_sign.this.signed_ref
images = {
app = cosign_sign.this.signed_ref
webhook = cosign_sign.webhook.signed_ref
}

github_app_id = 801323 // https://github.com/settings/apps/octosts
github_app_id = var.github_app_id
github_app_key_version = 1
notification_channels = local.notification_channels
}
3 changes: 3 additions & 0 deletions iac/terraform.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ project_id = "octo-sts"
regions = [
"us-central1",
]

// https://github.com/settings/apps/octosts
github_app_id = 801323
4 changes: 4 additions & 0 deletions iac/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ variable "regions" {
type = list(string)
default = []
}

variable "github_app_id" {
description = "The Github App ID for the Octo STS service."
}
2 changes: 1 addition & 1 deletion modules/app/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ module "this" {
service_account = google_service_account.octo-sts.email
containers = {
"sts" = {
image = var.image
image = var.images.app
ports = [{ container_port = 8080 }]
env = [
{
Expand Down
13 changes: 13 additions & 0 deletions modules/app/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
output "app" {
depends_on = [module.this]
value = {
name = var.name
}
}

output "webhook" {
depends_on = [module.webhook]
value = {
name = "${var.name}-webhook"
}
}
11 changes: 9 additions & 2 deletions modules/app/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@ variable "domain" {
type = string
}

variable "image" {
variable "images" {
description = "The Octo STS application image."
default = "chainguard/octo-sts:latest"
type = object({
app = optional(string, "chainguard/octo-sts:latest")
webhook = optional(string, "chainguard/octo-sts-webhook:latest")
})
default = {
app = "chainguard/octo-sts:latest"
webhook = "chainguard/octo-sts-webhook:latest"
}
}

variable "github_app_id" {
Expand Down
57 changes: 57 additions & 0 deletions modules/app/webhook.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Generate a random webhook secret
resource "random_password" "webhook-secret" {
length = 64
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}

module "webhook-secret" {
source = "chainguard-dev/common/infra//modules/configmap"
version = "0.6.18"

project_id = var.project_id
name = "${var.name}-webhook-secret"
data = random_password.webhook-secret.result

service-account = google_service_account.octo-sts.email

notification-channels = var.notification_channels
}

module "webhook" {
source = "chainguard-dev/common/infra//modules/regional-service"
version = "0.6.18"

project_id = var.project_id
name = "${var.name}-webhook"
regions = var.regions

// Only accept traffic coming from GCLB.
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
// This needs to egress in order to talk to Github
egress = "PRIVATE_RANGES_ONLY"

service_account = google_service_account.octo-sts.email
containers = {
"webhook" = {
image = var.images.webhook
ports = [{ container_port = 8080 }]
env = [
{
name = "GITHUB_APP_ID"
value = var.github_app_id
},
{
name = "GITHUB_WEBHOOK_SECRET"
value = module.webhook-secret.secret_version_id
},
{
name = "KMS_KEY"
value = local.kms_key
}
]
}
}

notification_channels = var.notification_channels
}
Loading

0 comments on commit 8f1603b

Please sign in to comment.