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:
parent
11b49ca02e
commit
3d75136654
9 changed files with 84 additions and 26 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
)
|
||||
|
||||
// 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)
|
||||
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())
|
||||
// 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, 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.Header.Add("Accept", "application/json")
|
||||
req.Header.Add(audHeader, audience)
|
||||
req.Header.Add(idHeader, identity)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue