1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-06 23:12:22 +00:00

widespread improvements

Signed-off-by: Jason Hall <jason@chainguard.dev>
This commit is contained in:
Jason Hall 2023-08-17 21:21:07 -04:00
parent 11b49ca02e
commit 3d75136654
Failed to extract signature
9 changed files with 84 additions and 26 deletions

View file

@ -9,3 +9,22 @@ The Terraform does everything:
- sets up a Chainguard Identity with permissions to pull from the private cgr.dev repo
- allows the Lambda function to assume the puller identity
- sets up a subscription to notify the Lambda function when pushes happen to cgr.dev
## Setup
```sh
aws sso login --profile my-profile
chainctl auth login
terraform init
terraform apply
```
This will prompt for a group ID and destination repo, and show you the resources it will create.
When the resources are created, any images that are pushed to your group will be mirrored to the ECR repository.
The Lambda function has minimal permissions: it's only allowed to push images to the destination repo.
The Chainguard identity also has minimal permissions: it only has permission to pull from the source repo.
To tear down resources, run `terraform destroy`.

View file

@ -11,6 +11,7 @@ import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
@ -22,39 +23,40 @@ var timeNow = time.Now
const (
audHeader = `Chainguard-Audience`
idHeader = `Chainguard-Identity`
// hashInit is the sha256 hash of an empty buffer, hex encoded.
hashInit = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
// STS service details for signing
svc = `sts`
)
// generateToken creates token using the supplied AWS credentials that can prove the user's AWS identity. Audience and identity are
// the Chainguard STS url (e.g https://issuer.enforce.dev) and the UID of the Chainguard assumable identity to assume via STS.
func generateToken(ctx context.Context, creds aws.Credentials, audience, identity string) (string, error) {
req, err := http.NewRequest("POST", "https://sts.amazonaws.com", nil)
func generateToken(ctx context.Context, creds aws.Credentials, region, audience, identity string) (string, error) {
url := (&url.URL{
Scheme: "https",
Host: "sts.amazonaws.com",
Path: "/",
RawQuery: url.Values{
"Action": []string{"GetCallerIdentity"},
"Version": []string{"2011-06-15"},
}.Encode(),
}).String()
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create new HTTP request: %w", err)
}
req.URL.Path = "/"
req.URL.RawQuery = "Action=GetCallerIdentity&Version=2011-06-15"
req.Header.Add("Accept", "application/json")
req.Header.Add(audHeader, audience)
req.Header.Add(idHeader, identity)
const (
// hashInit is the sha256 hash of an empty buffer, hex encoded.
hashInit = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
// STS service details for signing
svc = `sts`
region = `us-east-1`
)
err = v4.NewSigner().SignHTTP(ctx, creds, req, hashInit, svc, region, timeNow())
if err != nil {
if err := v4.NewSigner().SignHTTP(ctx, creds, req, hashInit, svc, region, timeNow()); err != nil {
return "", fmt.Errorf("failed to sign GetCallerIdentity request with AWS credentials: %w", err)
}
var b bytes.Buffer
err = req.Write(&b)
if err != nil {
if err := req.Write(&b); err != nil {
return "", fmt.Errorf("failed to serialize GetCallerIdentity HTTP request to buffer: %w", err)
}

View file

@ -13,6 +13,7 @@ import (
"io"
"log"
"net/http"
"net/url"
"path/filepath"
"strings"
@ -30,6 +31,7 @@ type envConfig struct {
Issuer string `envconfig:"ISSUER_URL" required:"true"`
Group string `envconfig:"GROUP" required:"true"`
Identity string `envconfig:"IDENTITY" required:"true"`
Region string `envconfig:"REGION" required:"true"`
Port int `envconfig:"PORT" default:"8080" required:"true"`
DstRepo string `envconfig:"DST_REPO" required:"true"`
}
@ -96,7 +98,7 @@ func main() {
if err := crane.Copy(src, dst,
crane.WithAuthFromKeychain(authn.NewMultiKeychain(
amazonKeychain,
cgKeychain{env.Issuer, env.Identity},
cgKeychain{env.Issuer, env.Region, env.Identity},
))); err != nil {
return fmt.Errorf("copying image: %w", err)
}
@ -118,7 +120,7 @@ func main() {
}
type cgKeychain struct {
issuer, identity string
issuer, region, identity string
}
func (k cgKeychain) Resolve(res authn.Resource) (authn.Authenticator, error) {
@ -136,12 +138,20 @@ func (k cgKeychain) Resolve(res authn.Resource) (authn.Authenticator, error) {
return nil, fmt.Errorf("failed to retrieve credentials, %w", err)
}
awsTok, err := generateToken(ctx, creds, res.RegistryStr(), k.identity)
awsTok, err := generateToken(ctx, creds, k.region, res.RegistryStr(), k.identity)
if err != nil {
return nil, fmt.Errorf("generating AWS token: %w", err)
}
url := fmt.Sprintf("%s/sts/exchange?aud=%s&identity=%s", k.issuer, res.RegistryStr(), k.identity)
url := (&url.URL{
Scheme: "https",
Host: k.issuer,
Path: "/sts/exchange",
RawQuery: url.Values{
"aud": []string{res.RegistryStr()},
"identity": []string{k.identity},
}.Encode(),
}).String()
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
return nil, err

View file

@ -38,10 +38,10 @@ locals {
// Using a local for the lambda breaks a cyclic dependency between
// chainguard_identity.aws and aws_lambda_function.lambda
lambda_name = "chainguard-lambda"
repo_name = var.dst_repo == "" ? local.lambda_name : var.dst_repo
}
data "aws_region" "current" {}
resource "aws_lambda_function" "lambda" {
function_name = local.lambda_name
role = aws_iam_role.lambda.arn
@ -55,6 +55,7 @@ resource "aws_lambda_function" "lambda" {
IDENTITY = chainguard_identity.aws.id
ISSUER_URL = "https://issuer.enforce.dev"
DST_REPO = aws_ecr_repository.ecr_repo.repository_url
REGION = data.aws_region.current.name
}
}
}

View file

@ -1,3 +1,7 @@
output "url" {
value = aws_lambda_function_url.lambda.function_url
}
output "dst_repo" {
value = aws_ecr_repository.ecr_repo.repository_url
}

View file

@ -5,6 +5,5 @@ variable "group" {
variable "dst_repo" {
type = string
default = "image-copy-ecr/lambda"
description = "The destination repo where images should be copied to."
}

View file

@ -9,3 +9,22 @@ The Terraform does everything:
- sets up a Chainguard Identity with permissions to pull from the private cgr.dev repo
- allows the Cloud Run service's SA to assume the puller identity
- sets up a subscription to notify the Cloud Run service when pushes happen to cgr.dev
# Setup
```sh
gcloud auth application-default login
chainctl auth login
terraform init
terraform apply
```
This will prompt for a group ID and destination repo, and show you the resources it will create.
When the resources are created, any images that are pushed to your group will be mirrored to the `gcr.io/<project-id>/<dst-repo>` repository.
The Cloud Run service account has minimal permissions: it's only allowed to push images to the destination repo.
The Chainguard identity also has minimal permissions: it only has permission to pull from the source repo.
To tear down resources, run `terraform destroy`.

View file

@ -1,3 +1,7 @@
output "url" {
value = google_cloud_run_service.image-copy.status[0].url
}
output "dst_repo" {
value = "${var.region}-docker.pkg.dev/${var.project}/${var.dst_repo}"
}

View file

@ -34,7 +34,7 @@ Then `terraform apply` the module (e.g., from the root of this repo):
```
module "image-workflow" {
source = "./image-workflow" # TODO: move to enforce-events
source = "./image-workflow/iac" # TODO: move to enforce-events
# name is used to prefix resources created by this demo application
# where possible.