Skip to content

Commit

Permalink
Merge pull request #2960 from buildkite/feat_kms_signing_and_verifying
Browse files Browse the repository at this point in the history
Support AWS KMS for signing and verifying pipelines
  • Loading branch information
wolfeidau authored Sep 2, 2024
2 parents b0b52ac + 059b464 commit b10f0c3
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 28 deletions.
7 changes: 3 additions & 4 deletions agent/agent_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package agent
import (
"regexp"
"time"

"github.com/lestrrat-go/jwx/v2/jwk"
)

// AgentConfiguration is the run-time configuration for an agent that
Expand Down Expand Up @@ -39,10 +37,11 @@ type AgentConfiguration struct {

SigningJWKSFile string // Where to find the key to sign pipeline uploads with (passed through to jobs, they might be uploading pipelines)
SigningJWKSKeyID string // The key ID to sign pipeline uploads with
SigningAWSKMSKey string // The KMS key ID to sign pipeline uploads with
DebugSigning bool // Whether to print step payloads when signing them

VerificationJWKS jwk.Set // The set of keys to verify jobs with
VerificationFailureBehaviour string // What to do if job verification fails (one of `block` or `warn`)
VerificationJWKS any // The set of keys to verify jobs with
VerificationFailureBehaviour string // What to do if job verification fails (one of `block` or `warn`)

ANSITimestamps bool
TimestampLines bool
Expand Down
8 changes: 6 additions & 2 deletions agent/job_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"github.com/buildkite/agent/v3/status"
"github.com/buildkite/roko"
"github.com/buildkite/shellwords"
"github.com/lestrrat-go/jwx/v2/jwk"
)

const (
Expand Down Expand Up @@ -85,7 +84,7 @@ type JobRunnerConfig struct {
JobStatusInterval time.Duration

// The JSON Web Keyset for verifying the job
JWKS jwk.Set
JWKS any

// A scope for metrics within a job
MetricsScope *metrics.Scope
Expand Down Expand Up @@ -524,6 +523,11 @@ func (r *JobRunner) createEnvironment(ctx context.Context) ([]string, error) {
env["BUILDKITE_PTY"] = "false"
}

// pass through the KMS key ID for signing
if r.conf.AgentConfiguration.SigningAWSKMSKey != "" {
env["BUILDKITE_AGENT_AWS_KMS_KEY"] = r.conf.AgentConfiguration.SigningAWSKMSKey
}

// Pass signing details through to the executor - any pipelines uploaded by this agent will be signed
if r.conf.AgentConfiguration.SigningJWKSFile != "" {
env["BUILDKITE_AGENT_JWKS_FILE"] = r.conf.AgentConfiguration.SigningJWKSFile
Expand Down
3 changes: 1 addition & 2 deletions agent/verify_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/buildkite/go-pipeline/signature"
"github.com/gowebpki/jcs"
"github.com/lestrrat-go/jwx/v2/jwk"
)

var (
Expand All @@ -35,7 +34,7 @@ func (e *invalidSignatureError) Unwrap() error {
return e.underlying
}

func (r *JobRunner) verifyJob(ctx context.Context, keySet jwk.Set) error {
func (r *JobRunner) verifyJob(ctx context.Context, keySet any) error {
step := r.conf.Job.Step

if step.Signature == nil {
Expand Down
46 changes: 43 additions & 3 deletions clicommand/agent_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ import (
"syscall"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/buildkite/agent/v3/agent"
"github.com/buildkite/agent/v3/api"
"github.com/buildkite/agent/v3/core"
"github.com/buildkite/agent/v3/internal/agentapi"
awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws"
"github.com/buildkite/agent/v3/internal/experiments"
"github.com/buildkite/agent/v3/internal/job/hook"
"github.com/buildkite/agent/v3/internal/job/shell"
Expand Down Expand Up @@ -83,8 +87,10 @@ type AgentStartConfig struct {
RedactedVars []string `cli:"redacted-vars" normalize:"list"`
CancelSignal string `cli:"cancel-signal"`

SigningJWKSFile string `cli:"signing-jwks-file" normalize:"filepath"`
SigningJWKSKeyID string `cli:"signing-jwks-key-id"`

SigningJWKSFile string `cli:"signing-jwks-file" normalize:"filepath"`
SigningAWSKMSKey string `cli:"signing-aws-kms-key"`
DebugSigning bool `cli:"debug-signing"`

VerificationJWKSFile string `cli:"verification-jwks-file" normalize:"filepath"`
Expand Down Expand Up @@ -658,6 +664,11 @@ var AgentStartCommand = cli.Command{
Usage: "The JWKS key ID to use when signing the pipeline. If omitted, and the signing JWKS contains only one key, that key will be used.",
EnvVar: "BUILDKITE_AGENT_SIGNING_JWKS_KEY_ID",
},
cli.StringFlag{
Name: "signing-aws-kms-key",
Usage: "The KMS KMS key ID, or key alias used when signing and verifying the pipeline.",
EnvVar: "BUILDKITE_AGENT_SIGNING_AWS_KMS_KEY",
},
cli.BoolFlag{
Name: "debug-signing",
Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled",
Expand Down Expand Up @@ -878,8 +889,36 @@ var AgentStartCommand = cli.Command{
defer shutdown()
}

var verificationJWKS jwk.Set
if cfg.VerificationJWKSFile != "" {
// if the agent is provided a KMS key ID, it should use the KMS signer, otherwise
// it should load the JWKS from the file
var verificationJWKS any
switch {
case cfg.SigningAWSKMSKey != "":

var logMode aws.ClientLogMode
// log requests and retries if we are debugging signing
// see https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/logging/
if cfg.DebugSigning {
logMode = aws.LogRetries | aws.LogRequest
}

// this is currently loaded here to ensure it is ONLY loaded if the agent is using KMS for signing
// this will limit the possible impact of this new SDK on the rest of the agent users
awscfg, err := config.LoadDefaultConfig(
ctx,
config.WithClientLogMode(logMode),
)
if err != nil {
return fmt.Errorf("failed to load AWS config: %w", err)
}

// assign a crypto signer which uses the KMS key to sign the pipeline
verificationJWKS, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey)
if err != nil {
return fmt.Errorf("couldn't create KMS signer: %w", err)
}

case cfg.VerificationJWKSFile != "":
var err error
verificationJWKS, err = parseAndValidateJWKS(ctx, "verification", cfg.VerificationJWKSFile)
if err != nil {
Expand Down Expand Up @@ -958,6 +997,7 @@ var AgentStartCommand = cli.Command{

SigningJWKSFile: cfg.SigningJWKSFile,
SigningJWKSKeyID: cfg.SigningJWKSKeyID,
SigningAWSKMSKey: cfg.SigningAWSKMSKey,
DebugSigning: cfg.DebugSigning,

VerificationJWKS: verificationJWKS,
Expand Down
39 changes: 34 additions & 5 deletions clicommand/pipeline_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import (
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/buildkite/agent/v3/agent"
"github.com/buildkite/agent/v3/api"
"github.com/buildkite/agent/v3/env"
awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws"
"github.com/buildkite/agent/v3/internal/experiments"
"github.com/buildkite/agent/v3/internal/redact"
"github.com/buildkite/agent/v3/internal/replacer"
Expand Down Expand Up @@ -76,9 +79,10 @@ type PipelineUploadConfig struct {
RejectSecrets bool `cli:"reject-secrets"`

// Used for signing
JWKSFile string `cli:"jwks-file"`
JWKSKeyID string `cli:"jwks-key-id"`
DebugSigning bool `cli:"debug-signing"`
JWKSFile string `cli:"jwks-file"`
JWKSKeyID string `cli:"jwks-key-id"`
SigningAWSKMSKey string `cli:"signing-aws-kms-key"`
DebugSigning bool `cli:"debug-signing"`

// Global flags
Debug bool `cli:"debug"`
Expand Down Expand Up @@ -144,6 +148,11 @@ var PipelineUploadCommand = cli.Command{
Usage: "The JWKS key ID to use when signing the pipeline. Required when using a JWKS",
EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID",
},
cli.StringFlag{
Name: "signing-aws-kms-key",
Usage: "The AWS KMS key identifier which is used to sign pipelines.",
EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY",
},
cli.BoolFlag{
Name: "debug-signing",
Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled",
Expand Down Expand Up @@ -275,11 +284,31 @@ var PipelineUploadCommand = cli.Command{
searchForSecrets(l, &cfg, environ, result, src)
}

if cfg.JWKSFile != "" {
key, err := jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
var (
key signature.Key
)

switch {
case cfg.SigningAWSKMSKey != "":
awscfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return fmt.Errorf("couldn't load AWS config: %w", err)
}

// assign a crypto signer which uses the KMS key to sign the pipeline
key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.SigningAWSKMSKey)
if err != nil {
return fmt.Errorf("couldn't create KMS signer: %w", err)
}

case cfg.JWKSFile != "":
key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
if err != nil {
return fmt.Errorf("couldn't read the signing key file: %w", err)
}
}

if key != nil {

err = signature.SignSteps(
ctx,
Expand Down
52 changes: 43 additions & 9 deletions clicommand/tool_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import (
"os"
"strings"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/buildkite/agent/v3/internal/bkgql"
awssigner "github.com/buildkite/agent/v3/internal/cryptosigner/aws"
"github.com/buildkite/agent/v3/internal/stdin"
"github.com/buildkite/agent/v3/logger"
"github.com/buildkite/go-pipeline"
"github.com/buildkite/go-pipeline/jwkutil"
"github.com/buildkite/go-pipeline/signature"
"github.com/buildkite/go-pipeline/warning"
"github.com/buildkite/interpolate"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/urfave/cli"
"gopkg.in/yaml.v3"
)
Expand All @@ -31,9 +33,14 @@ type ToolSignConfig struct {
NoConfirm bool `cli:"no-confirm"`

// Used for signing
JWKSFile string `cli:"jwks-file"`
JWKSKeyID string `cli:"jwks-key-id"`
DebugSigning bool `cli:"debug-signing"`
JWKSFile string `cli:"jwks-file"`
JWKSKeyID string `cli:"jwks-key-id"`

// AWS KMS key used for signing pipelines
AWSKMSKeyID string `cli:"signing-aws-kms-key"`

// Enable debug logging for pipeline signing, this depends on debug logging also being enabled
DebugSigning bool `cli:"debug-signing"`

// Needed for to use GraphQL API
OrganizationSlug string `cli:"organization-slug"`
Expand Down Expand Up @@ -127,6 +134,11 @@ Signing a pipeline from a file:
Usage: "The JWKS key ID to use when signing the pipeline. If none is provided and the JWKS file contains only one key, that key will be used.",
EnvVar: "BUILDKITE_AGENT_JWKS_KEY_ID",
},
cli.StringFlag{
Name: "signing-aws-kms-key",
Usage: "The AWS KMS key identifier which is used to sign pipelines.",
EnvVar: "BUILDKITE_AGENT_AWS_KMS_KEY",
},
cli.BoolFlag{
Name: "debug-signing",
Usage: "Enable debug logging for pipeline signing. This can potentially leak secrets to the logs as it prints each step in full before signing. Requires debug logging to be enabled",
Expand Down Expand Up @@ -170,9 +182,31 @@ Signing a pipeline from a file:
ctx, cfg, l, _, done := setupLoggerAndConfig[ToolSignConfig](context.Background(), c)
defer done()

key, err := jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
if err != nil {
return fmt.Errorf("couldn't read the signing key file: %w", err)
var (
key signature.Key
err error
)

switch {
case cfg.AWSKMSKeyID != "":
// load the AWS SDK V2 config
awscfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return fmt.Errorf("couldn't load AWS config: %w", err)
}

// assign a crypto signer which uses the KMS key to sign the pipeline
key, err = awssigner.NewKMS(kms.NewFromConfig(awscfg), cfg.AWSKMSKeyID)
if err != nil {
return fmt.Errorf("couldn't create KMS signer: %w", err)
}

default:
key, err = jwkutil.LoadKey(cfg.JWKSFile, cfg.JWKSKeyID)
if err != nil {
return fmt.Errorf("couldn't read the signing key file: %w", err)
}

}

sign := signWithGraphQL
Expand Down Expand Up @@ -209,7 +243,7 @@ func validateNoInterpolations(pipelineString string) error {
return nil
}

func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.Key, cfg *ToolSignConfig) error {
func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key signature.Key, cfg *ToolSignConfig) error {
if cfg.Repository == "" {
return ErrUseGraphQL
}
Expand Down Expand Up @@ -289,7 +323,7 @@ func signOffline(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.K
return enc.Encode(parsedPipeline)
}

func signWithGraphQL(ctx context.Context, c *cli.Context, l logger.Logger, key jwk.Key, cfg *ToolSignConfig) error {
func signWithGraphQL(ctx context.Context, c *cli.Context, l logger.Logger, key signature.Key, cfg *ToolSignConfig) error {
orgPipelineSlug := fmt.Sprintf("%s/%s", cfg.OrganizationSlug, cfg.PipelineSlug)
debugL := l.WithFields(logger.StringField("orgPipelineSlug", orgPipelineSlug))

Expand Down
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ require (
github.com/DrJosh9000/zzglob v0.3.4
github.com/Khan/genqlient v0.7.0
github.com/aws/aws-sdk-go v1.55.5
github.com/aws/aws-sdk-go-v2 v1.30.4
github.com/aws/aws-sdk-go-v2/config v1.27.30
github.com/aws/aws-sdk-go-v2/service/kms v1.35.5
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
github.com/buildkite/bintest/v3 v3.3.0
github.com/buildkite/go-pipeline v0.12.0
github.com/buildkite/go-pipeline v0.13.0
github.com/buildkite/interpolate v0.1.3
github.com/buildkite/roko v1.2.0
github.com/buildkite/shellwords v0.0.0-20180315084142-c3f497d1e000
Expand Down Expand Up @@ -75,6 +78,17 @@ require (
github.com/alexflint/go-arg v1.4.2 // indirect
github.com/alexflint/go-scalar v1.0.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.29 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
Expand Down
Loading

0 comments on commit b10f0c3

Please sign in to comment.