-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Martin Baillie <[email protected]>
- Loading branch information
0 parents
commit 1b5ea3c
Showing
25 changed files
with
3,859 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
end_of_line = lf | ||
indent_size = 4 | ||
indent_style = tab | ||
insert_final_newline = true | ||
max_line_length = 80 | ||
trim_trailing_whitespace = true | ||
|
||
[*.{md,org}] | ||
max_line_length = 0 | ||
trim_trailing_whitespace = false | ||
|
||
[*.{yml,yaml}] | ||
indent_size = 2 | ||
indent_style = space | ||
|
||
[*.go] | ||
max_line_length = 80 ; Just for comments | ||
|
||
[{Makefile,*.bash}] | ||
indent_size = 2 | ||
max_line_length = 80 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
if ! has nix_direnv_version || ! nix_direnv_version 1.4.0; then | ||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.4.0/direnvrc" "sha256-1VWM1BnI1GvclYBky5f5Y9HqeThmQUwCWQbsFQM1Eu0=" | ||
fi | ||
use flake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: release | ||
|
||
on: | ||
push: | ||
tags: ["*"] | ||
|
||
jobs: | ||
goreleaser: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- uses: actions/setup-go@v1 | ||
with: | ||
go-version: 1.16.x | ||
- uses: goreleaser/goreleaser-action@v2 | ||
with: | ||
version: latest | ||
args: release --rm-dist | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.DS_Store | ||
*.log | ||
tmp/ | ||
.direnv | ||
cdk.out | ||
env/.bootstrap |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
archives: | ||
- id: ocistow | ||
format: binary | ||
builds: | ||
- id: ocistow | ||
env: | ||
- CGO_ENABLED=0 | ||
main: ./cmd/ocistow/main.go | ||
binary: ocistow | ||
flags: | ||
- -trimpath | ||
goarch: | ||
- amd64 | ||
- arm64 | ||
goos: | ||
- linux | ||
- darwin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
SHELL := $(shell which bash) | ||
.SHELLFLAGS = -o pipefail -c | ||
.EXPORT_ALL_VARIABLES: ; | ||
ifndef DEBUG | ||
.SILENT: ; | ||
endif | ||
|
||
env/.bootstrap: ; npx cdk bootstrap && touch $@ | ||
deploy: env/.bootstrap; npx cdk deploy | ||
destroy: ; npx cdk destroy | ||
.PHONY: deploy destroy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
* ocistow :TOC_3_gh: | ||
- [[#about][About]] | ||
- [[#how-it-works][How it works]] | ||
- [[#try-it-yourself][Try it yourself]] | ||
- [[#prerequisites][Prerequisites]] | ||
- [[#cli-cmdocistow][CLI (cmd/ocistow)]] | ||
- [[#lambda-cmdocistow-lambda][Lambda (cmd/ocistow-lambda)]] | ||
- [[#deploy][Deploy]] | ||
- [[#invoke][Invoke]] | ||
- [[#verify-signatures-with-cosign][Verify signatures with =cosign=]] | ||
- [[#insight][Insight]] | ||
|
||
* About | ||
The =ocistow= codebase demonstrates a modern approach to a typical OCI image | ||
promotion workflow in AWS. | ||
|
||
It houses a Lambda (and bonus CLI) that can efficiently stream and mutate | ||
upstream container image layers into an ECR destination and subsequently sign | ||
them with KMS. It does so by utilising code from the excellent | ||
[[https://github.com/google/go-containerregistry][go-containerregistry]] and [[https://github.com/sigstore][sigstore]] projects. | ||
|
||
* How it works | ||
[[img/ocistow.png][img/ocistow.png]] | ||
Given an invoke payload of: | ||
1. A source image reference (any public container registry / private ECR) | ||
2. A destination image reference (private ECR) | ||
3. Some annotations to add | ||
|
||
It will: | ||
1. Stream only the missing image layers from source registry to destination ECR whilst handling ECR authentication | ||
2. Do so in memory, layer-by-layer[fn:1] (with Lambda's meagre 512mb filesystem remaining unused) | ||
3. Optionally mutate the image during this process to have user provided OCI | ||
annotations and legacy Docker image labels (mimicking the sort of mandatory | ||
tagging policy an organisation might have) | ||
4. And finally sign the image digests in AWS ECR using a KMS signing key for | ||
later assertion of provenance at runtime (e.g. using a Kubernetes admission | ||
controller like [[https://github.com/dlorenc/cosigned][cosigned]]). | ||
|
||
[fn:1]: Performance gains can be had by throwing more memory at the Lambda as | ||
this results in more allocated CPU and critically, network (at AWS' discretion). | ||
Empirically (though not very scientifically), I saw the following with the | ||
massive *3+ gigabyte* TensorFlow images from [[https://gcr.io][gcr.io]]. | ||
|
||
- Test 1 (vanilla Lambda settings 128mb memory): 8.06 minutes | ||
- Test 2 (maxed out Lambda settings 10240mb memory): *1.35 minutes* | ||
|
||
No shared layers existed in my destination ECR between tests—all blobs were | ||
streamed from source to destination. | ||
|
||
#+begin_quote | ||
NOTE: This would be interesting to give a run through the [[https://github.com/alexcasalboni/aws-lambda-power-tuning][AWS Lambda Power Tuner]]. | ||
#+end_quote | ||
* Try it yourself | ||
I can’t imagine anyone using the Lambda (nor CLI) verbatim in their workflow | ||
unless it happened to solve an exact gap (let me know if you do!), but the | ||
codebase may be a useful reference for informing your own build. | ||
|
||
For example, a Lambda /like/ =ocistow= could be that final "promotion" step in | ||
an organisation's container image supply chain which first involves the image | ||
running a gauntlet of vulnerability/malware/compliance scans in a Step Function | ||
state machine. | ||
|
||
However, outside of the Lambda space, CLIs like Google's [[https://https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md][crane]] and Sigstore's | ||
[[https://github.com/sigstore/cosign][cosign]] are much more polished and suitable to a range of container image | ||
copy/mutate/sign workflows. You should check them out. | ||
** Prerequisites | ||
An AWS account with a KMS signing key and ECR repository for playing with. The | ||
following =aws= incantations will do the trick presuming you have an | ||
appropriately privileged session. | ||
|
||
#+begin_src shell | ||
aws kms create-key \ | ||
--key-usage SIGN_VERIFY \ | ||
--customer-master-key-spec RSA_4096 \ | ||
--tags TagKey=Name,TagValue=ocistow \ | ||
--description "ocistow demo" | ||
|
||
aws ecr create-repository --repository-name ocistow-demo | ||
#+end_src | ||
** CLI (cmd/ocistow) | ||
As I was extracting the Lambda out from some larger research to post here, I | ||
realised it would be easy to add a CLI. The =ocistow= CLI can do the same thing | ||
as the Lambda but from your local machine. Though, unless it fits your exact use | ||
case, you may wish to just reference it for your own CLI implementation or otherwise | ||
reach for the much more practical Google [[https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md][crane]]. | ||
|
||
In any case, you can get a build of =ocistow= from the [[https://github.com/martinbaillie/ocistow/releases][releases]] section or build and run | ||
yourself (the repository is a [[./flake.nix][Nix flake]] if that's your thing, otherwise you'll | ||
want a local Go 1.16+ toolchain): | ||
|
||
#+begin_src shell | ||
go <build|install|run> github.com/martinbaillie/ocistow/cmd/ocistow -h | ||
#+end_src | ||
|
||
#+begin_example | ||
Usage of ocistow: | ||
-annotations value | ||
destination image annotations (key=value) | ||
-aws-kms-key-arn string | ||
AWS KMS key ARN to use for signing | ||
-aws-region string | ||
AWS region to use for operations | ||
-aws-xray | ||
whether to enable AWS Xray tracing | ||
-copy | ||
whether to copy the image (default true) | ||
-debug | ||
debug logging | ||
-destination string | ||
destination image | ||
-sign | ||
whether to sign the image (default true) | ||
-source string | ||
source image | ||
#+end_example | ||
|
||
Kick the tyres by stowing DockerHub's =busybox:latest= into the demo ECR repository: | ||
#+begin_src shell | ||
ocistow \ | ||
-source=busybox \ | ||
-destination=111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo \ | ||
-aws-region=ap-southeast-2 \ | ||
-aws-kms-key-arn="<ARN from Prerequisites>" \ | ||
-annotations team=foo \ | ||
-annotations owner=martin | ||
#+end_src | ||
|
||
#+begin_example | ||
21:09:00.000 annotations={"owner":"martin","team":"foo"} component=service dst=111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo method=Copy src=busybox took=2.999482083s | ||
21:09:00.000 annotations={"owner":"martin","team":"foo"} component=service dst=111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo method=Sign took=878.351667ms | ||
#+end_example | ||
|
||
** Lambda (cmd/ocistow-lambda) | ||
*** Deploy | ||
For playing with the =ocistow-lambda= in your AWS account you can use the [[./env][CDK | ||
deployment]] in this repository. There's a [[./Makefile][Makefile]] target that can kick this | ||
process off (though like the CLI instructions above, either use the Nix flake or | ||
get yourself Go 1.16+ and additionally NodeJS for the CDK). | ||
|
||
#+begin_src shell | ||
make deploy AWS_KMS_KEY_ARN="<ARN from Prerequisites>" | ||
#+end_src | ||
|
||
This will build and deploy an aarch64/Graviton version of the Lambda to your | ||
account with necessary KMS/ECR permissions. Take note of the function ARN output | ||
for later invocation. | ||
*** Invoke | ||
Kick the tyres by stowing DockerHub's =busybox:latest= into the demo ECR repository: | ||
|
||
#+begin_quote | ||
NOTE: The Lambda expects a very simple [[https://github.com/martinbaillie/ocistow/blob/main/pkg/transport/lambda.go#L15-L19][JSON schema]] as its payload. | ||
#+end_quote | ||
|
||
#+begin_src shell | ||
aws lambda invoke \ | ||
--function-name "arn:aws:lambda:ap-southeast-2:111111111111:function:ocistow-function" --cli-binary-format raw-in-base64-out \ | ||
--payload '{ | ||
"SrcImageRef":"busybox", | ||
"DstImageRef": "111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo", | ||
"Annotations":{"team":"foo", "owner":"martin"} | ||
}' /dev/stderr | ||
#+end_src | ||
|
||
#+begin_example | ||
{ | ||
"StatusCode": 200, | ||
"ExecutedVersion": "$LATEST" | ||
} | ||
#+end_example | ||
|
||
** Verify signatures with =cosign= | ||
#+begin_src shell | ||
AWS_REGION=ap-southeast-2 cosign verify \ | ||
-key "<ARN from Prerequisites>" \ | ||
111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo | ||
#+end_src | ||
|
||
#+begin_example | ||
Verification for 111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo -- | ||
The following checks were performed on each of these signatures: | ||
- The cosign claims were validated | ||
- The signatures were verified against the specified public key | ||
- Any certificates were verified against the Fulcio roots. | ||
|
||
[{"critical":{"identity":{"docker-reference":"111111111111.dkr.ecr.ap-southeast-2.amazonaws.com/ocistow-demo"},"image":{"docker-manifest-digest":"sha256:ee16ac0396cdb32e870200cdcb30f9abcb6b95256e5b5cd57eb1fadf2d3b3c9d"},"type":"cosign container image signature"},"optional":{"team":"foo","owner":"martin"}}] | ||
#+end_example | ||
|
||
** Insight | ||
A [[https://stripe.com/blog/canonical-log-lines][canonical log line]] is output for each service method (Copy, Sign) which you'll find on the terminal output for CLI and in CloudWatch for Lambda. | ||
|
||
If debug logging is enabled (flag: =-debug=, env: =DEBUG=) then much more | ||
detailed output is made available from the backend libraries used. | ||
|
||
If AWS Xray is enabled (flag: =-aws-xray=, env: =AWS_XRAY=) then detailed traces | ||
of the layer-by-layer =ocistow= operations are also propagated: | ||
|
||
[[img/segments.png][img/segments.png]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"app": "go mod download && go run env/main.go", | ||
"context": { | ||
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, | ||
"@aws-cdk/core:enableStackNameDuplicates": "true", | ||
"aws-cdk:enableDiffNoFail": "true", | ||
"@aws-cdk/core:stackRelativeExports": "true", | ||
"@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, | ||
"@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, | ||
"@aws-cdk/aws-kms:defaultKeyPolicies": true, | ||
"@aws-cdk/aws-s3:grantWriteWithoutAcl": true, | ||
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, | ||
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true, | ||
"@aws-cdk/aws-efs:defaultEncryptionAtRest": true, | ||
"@aws-cdk/aws-lambda:recognizeVersionProps": true, | ||
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"path" | ||
|
||
"github.com/aws/aws-lambda-go/lambda" | ||
|
||
"github.com/martinbaillie/ocistow/pkg/backend" | ||
"github.com/martinbaillie/ocistow/pkg/config" | ||
"github.com/martinbaillie/ocistow/pkg/service" | ||
"github.com/martinbaillie/ocistow/pkg/transport" | ||
) | ||
|
||
func run(argv []string, out *os.File) error { | ||
cfg := config.New(path.Base(argv[0]), out) | ||
|
||
if err := cfg.Parse(argv[1:]); err != nil { | ||
return fmt.Errorf("parsing config: %w", err) | ||
} | ||
|
||
svc := service.NewService(backend.NewAWS(*cfg.AWSKMSKeyARN, *cfg.AWSRegion, *cfg.AWSXray)) | ||
|
||
if *cfg.AWSXray { | ||
svc = service.NewAWSXrayMiddleware()(svc) | ||
} | ||
|
||
svc = service.NewContextLoggerMiddleware()(svc) | ||
|
||
lambda.Start(transport.NewStowLambdaHandler(cfg, svc)) | ||
|
||
return nil | ||
} | ||
|
||
func main() { | ||
if err := run(os.Args, os.Stderr); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"path" | ||
|
||
"github.com/martinbaillie/ocistow/pkg/backend" | ||
"github.com/martinbaillie/ocistow/pkg/config" | ||
"github.com/martinbaillie/ocistow/pkg/service" | ||
"github.com/martinbaillie/ocistow/pkg/transport" | ||
) | ||
|
||
func run(argv []string, out *os.File) error { | ||
cfg := config.New(path.Base(argv[0]), out) | ||
|
||
src := cfg.String("source", "", "source image") | ||
dst := cfg.String("destination", "", "destination image") | ||
annotations := cfg.StringMap("annotations", "destination image annotations (key=value)") | ||
|
||
if err := cfg.Parse(argv[1:]); err != nil { | ||
return fmt.Errorf("parsing config: %w", err) | ||
} | ||
|
||
svc := service.NewService(backend.NewAWS(*cfg.AWSKMSKeyARN, *cfg.AWSRegion, *cfg.AWSXray)) | ||
|
||
if *cfg.AWSXray { | ||
svc = service.NewAWSXrayMiddleware()(svc) | ||
} | ||
|
||
svc = service.NewContextLoggerMiddleware()(svc) | ||
|
||
return transport.NewCLI(cfg, svc).Stow(*src, *dst, *annotations) | ||
} | ||
|
||
func main() { | ||
if err := run(os.Args, os.Stderr); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
Oops, something went wrong.