Skip to content

Commit

Permalink
Add webhook to validate STS configurations at pull request.
Browse files Browse the repository at this point in the history
Since we don't give any information at request time about why a STS
policy is valid or not so that we don't leak details about the policy,
this webhook will validate on the repo at pull request / push so the
repo authors get feedback on potential config issues.
  • Loading branch information
wlynch authored and mattmoor committed May 18, 2024
1 parent 91be077 commit fc6d457
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 fc6d457

Please sign in to comment.