diff --git a/cmd/vela-server/queue.go b/cmd/vela-server/queue.go index 677137e18..edd703e69 100644 --- a/cmd/vela-server/queue.go +++ b/cmd/vela-server/queue.go @@ -18,11 +18,12 @@ func setupQueue(c *cli.Context) (queue.Service, error) { // queue configuration _setup := &queue.Setup{ - Driver: c.String("queue.driver"), - Address: c.String("queue.addr"), - Cluster: c.Bool("queue.cluster"), - Routes: c.StringSlice("queue.routes"), - Timeout: c.Duration("queue.pop.timeout"), + Driver: c.String("queue.driver"), + Address: c.String("queue.addr"), + Cluster: c.Bool("queue.cluster"), + Routes: c.StringSlice("queue.routes"), + Timeout: c.Duration("queue.pop.timeout"), + PrivateKey: c.String("queue.private-key"), } // setup the queue diff --git a/cmd/vela-server/server.go b/cmd/vela-server/server.go index d18a74278..cb9f0edd1 100644 --- a/cmd/vela-server/server.go +++ b/cmd/vela-server/server.go @@ -98,6 +98,7 @@ func server(c *cli.Context) error { middleware.Secret(c.String("vela-secret")), middleware.Secrets(secrets), middleware.Scm(scm), + middleware.QueueSigningPrivateKey(c.String("queue.private-key")), middleware.Allowlist(c.StringSlice("vela-repo-allowlist")), middleware.DefaultBuildLimit(c.Int64("default-build-limit")), middleware.DefaultTimeout(c.Int64("default-build-timeout")), diff --git a/docker-compose.yml b/docker-compose.yml index 09e729ee8..824b461a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: VELA_USER_ACCESS_TOKEN_DURATION: 60m VELA_DISABLE_WEBHOOK_VALIDATION: 'true' VELA_ENABLE_SECURE_COOKIE: 'false' + VELA_QUEUE_SIGNING_PRIVATE_KEY: 'tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==' VELA_REPO_ALLOWLIST: '*' VELA_SCHEDULE_ALLOWLIST: '*' env_file: @@ -78,6 +79,7 @@ services: VELA_SERVER_SECRET: 'zB7mrKDTZqNeNTD8z47yG4DHywspAh' WORKER_ADDR: 'http://worker:8080' WORKER_CHECK_IN: 5m + VELA_QUEUE_SIGNING_PUBLIC_KEY: 'DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=' restart: always ports: - '8081:8080' diff --git a/go.mod b/go.mod index addeed1ab..788f5cdb0 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/spf13/afero v1.9.5 github.com/urfave/cli/v2 v2.25.7 go.starlark.net v0.0.0-20230725161458-0d7263928a74 + golang.org/x/crypto v0.11.0 golang.org/x/oauth2 v0.9.0 golang.org/x/sync v0.3.0 gopkg.in/square/go-jose.v2 v2.6.0 @@ -116,7 +117,6 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect diff --git a/queue/flags.go b/queue/flags.go index 0fd1e23a6..9f2c508ee 100644 --- a/queue/flags.go +++ b/queue/flags.go @@ -50,4 +50,16 @@ var Flags = []cli.Flag{ Usage: "timeout for requests that pop items off the queue", Value: 60 * time.Second, }, + &cli.StringFlag{ + EnvVars: []string{"VELA_QUEUE_SIGNING_PRIVATE_KEY"}, + FilePath: "/vela/signing.key", + Name: "queue.private-key", + Usage: "set value of base64 encoded queue signing private key", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_QUEUE_SIGNING_PUBLIC_KEY"}, + FilePath: "/vela/signing.pub", + Name: "queue.public-key", + Usage: "set value of base64 encoded queue signing public key", + }, } diff --git a/queue/redis/length_test.go b/queue/redis/length_test.go index c97a5b3f8..d9622594c 100644 --- a/queue/redis/length_test.go +++ b/queue/redis/length_test.go @@ -29,7 +29,7 @@ func TestRedis_Length(t *testing.T) { } // setup redis mock - _redis, err := NewTest("vela", "vela:second", "vela:third") + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela", "vela:second", "vela:third") if err != nil { t.Errorf("unable to create queue service: %v", err) } diff --git a/queue/redis/opts.go b/queue/redis/opts.go index aabcfea05..5589eeb92 100644 --- a/queue/redis/opts.go +++ b/queue/redis/opts.go @@ -5,6 +5,8 @@ package redis import ( + "encoding/base64" + "errors" "fmt" "time" ) @@ -69,3 +71,75 @@ func WithTimeout(timeout time.Duration) ClientOpt { return nil } } + +// WithPrivateKey sets the private key in the queue client for Redis. +// +//nolint:dupl // ignore similar code +func WithPrivateKey(key string) ClientOpt { + return func(c *client) error { + c.Logger.Trace("configuring private key in redis queue client") + + if len(key) == 0 { + c.Logger.Warn("unable to base64 decode private key, provided key is empty. queue service will be unable to sign items") + return nil + } + + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + + if len(decoded) == 0 { + return errors.New("unable to base64 decode private key, decoded key is empty") + } + + c.config.PrivateKey = new([64]byte) + copy(c.config.PrivateKey[:], decoded) + + if c.config.PrivateKey == nil { + return errors.New("unable to copy decoded queue signing private key, copied key is nil") + } + + if len(c.config.PrivateKey) == 0 { + return errors.New("unable to copy decoded queue signing private key, copied key is empty") + } + + return nil + } +} + +// WithPublicKey sets the public key in the queue client for Redis. +// +//nolint:dupl // ignore similar code +func WithPublicKey(key string) ClientOpt { + return func(c *client) error { + c.Logger.Tracef("configuring public key in redis queue client") + + if len(key) == 0 { + c.Logger.Warn("unable to base64 decode public key, provided key is empty. queue service will be unable to open items") + return nil + } + + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + + if len(decoded) == 0 { + return errors.New("unable to base64 decode public key, decoded key is empty") + } + + c.config.PublicKey = new([32]byte) + copy(c.config.PublicKey[:], decoded) + + if c.config.PublicKey == nil { + return errors.New("unable to copy decoded queue signing public key, copied key is nil") + } + + if len(c.config.PublicKey) == 0 { + return errors.New("unable to copy decoded queue signing public key, copied key is empty") + } + + return nil + } +} diff --git a/queue/redis/opts_test.go b/queue/redis/opts_test.go index b44e79418..3f99e3857 100644 --- a/queue/redis/opts_test.go +++ b/queue/redis/opts_test.go @@ -5,6 +5,7 @@ package redis import ( + "encoding/base64" "fmt" "reflect" "testing" @@ -180,3 +181,139 @@ func TestRedis_ClientOpt_WithCluster(t *testing.T) { } } } + +func TestRedis_ClientOpt_WithSigningPrivateKey(t *testing.T) { + // setup tests + // create a local fake redis instance + // + // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run + _redis, err := miniredis.Run() + if err != nil { + t.Errorf("unable to create miniredis instance: %v", err) + } + defer _redis.Close() + + tests := []struct { + failure bool + privKey string + want string + }{ + { //valid key input + failure: false, + privKey: "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==", + want: "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==", + }, + { //empty key input + failure: false, + privKey: "", + want: "", + }, + { //invalid base64 encoded input + failure: true, + privKey: "abc123", + want: "", + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), + WithPrivateKey(test.privKey), + ) + + if test.failure { + if err == nil { + t.Errorf("WithPrivateKey should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithPrivateKey returned err: %v", err) + } + + got := "" + if _service.config.PrivateKey != nil { + got = fmt.Sprintf("%s", *_service.config.PrivateKey) + } else { + got = "" + } + + w, _ := base64.StdEncoding.DecodeString(test.want) + + want := string(w) + if !reflect.DeepEqual(got, want) { + t.Errorf("WithPrivateKey is %v, want %v", got, want) + } + } +} + +func TestRedis_ClientOpt_WithSigningPublicKey(t *testing.T) { + // setup tests + // create a local fake redis instance + // + // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run + _redis, err := miniredis.Run() + if err != nil { + t.Errorf("unable to create miniredis instance: %v", err) + } + defer _redis.Close() + + tests := []struct { + failure bool + pubKey string + want string + }{ + { //valid key input + failure: false, + pubKey: "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=", + want: "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=", + }, + { //empty key input + failure: false, + pubKey: "", + want: "", + }, + { //invalid base64 encoded input + failure: true, + pubKey: "abc123", + want: "", + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), + WithPublicKey(test.pubKey), + ) + + if test.failure { + if err == nil { + t.Errorf("WithPublicKey should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithPublicKey returned err: %v", err) + } + + got := "" + if _service.config.PublicKey != nil { + got = fmt.Sprintf("%s", *_service.config.PublicKey) + } else { + got = "" + } + + w, _ := base64.StdEncoding.DecodeString(test.want) + + want := string(w) + if !reflect.DeepEqual(got, want) { + t.Errorf("SigningPublicKey is %v, want %v", got, want) + } + } +} diff --git a/queue/redis/pop.go b/queue/redis/pop.go index 88151649b..732092c32 100644 --- a/queue/redis/pop.go +++ b/queue/redis/pop.go @@ -11,6 +11,7 @@ import ( "github.com/go-vela/types" "github.com/redis/go-redis/v9" + "golang.org/x/crypto/nacl/sign" ) // Pop grabs an item from the specified channel off the queue. @@ -35,10 +36,28 @@ func (c *client) Pop(ctx context.Context) (*types.Item, error) { return nil, err } - item := new(types.Item) + // this should already be validated on startup + if c.config.PublicKey == nil || len(*c.config.PublicKey) != 32 { + return nil, errors.New("no valid signing public key provided") + } + + // extract signed item from pop results + signed := []byte(result[1]) + + var opened, out []byte + + // open the item using the public key generated using sign + // + // https://pkg.go.dev/golang.org/x/crypto@v0.1.0/nacl/sign + opened, ok := sign.Open(out, signed, c.config.PublicKey) + if !ok { + return nil, errors.New("unable to open signed item") + } // unmarshal result into queue item - err = json.Unmarshal([]byte(result[1]), item) + item := new(types.Item) + + err = json.Unmarshal(opened, item) if err != nil { return nil, err } diff --git a/queue/redis/pop_test.go b/queue/redis/pop_test.go index cb8209e1d..1f19bf2c3 100644 --- a/queue/redis/pop_test.go +++ b/queue/redis/pop_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/go-vela/types" + "golang.org/x/crypto/nacl/sign" "gopkg.in/square/go-jose.v2/json" ) @@ -23,6 +24,9 @@ func TestRedis_Pop(t *testing.T) { User: _user, } + var signed []byte + var out []byte + // setup queue item bytes, err := json.Marshal(_item) if err != nil { @@ -30,19 +34,21 @@ func TestRedis_Pop(t *testing.T) { } // setup redis mock - _redis, err := NewTest("vela") + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } + signed = sign.Sign(out, bytes, _redis.config.PrivateKey) + // push item to queue - err = _redis.Redis.RPush(context.Background(), "vela", bytes).Err() + err = _redis.Redis.RPush(context.Background(), "vela", signed).Err() if err != nil { t.Errorf("unable to push item to queue: %v", err) } // setup timeout redis mock - timeout, err := NewTest("vela") + timeout, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } @@ -50,15 +56,17 @@ func TestRedis_Pop(t *testing.T) { timeout.config.Timeout = 1 * time.Second // setup badChannel redis mock - badChannel, err := NewTest("vela") + badChannel, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } // overwrite channel to be invalid badChannel.config.Channels = nil + signed = sign.Sign(out, bytes, badChannel.config.PrivateKey) + // push something to badChannel queue - err = badChannel.Redis.RPush(context.Background(), "vela", bytes).Err() + err = badChannel.Redis.RPush(context.Background(), "vela", signed).Err() if err != nil { t.Errorf("unable to push item to queue: %v", err) } diff --git a/queue/redis/push.go b/queue/redis/push.go index 7ba97654f..66c8a386a 100644 --- a/queue/redis/push.go +++ b/queue/redis/push.go @@ -7,6 +7,8 @@ package redis import ( "context" "errors" + + "golang.org/x/crypto/nacl/sign" ) // Push inserts an item to the specified channel in the queue. @@ -21,10 +23,26 @@ func (c *client) Push(ctx context.Context, channel string, item []byte) error { return errors.New("item is nil") } + var signed []byte + + var out []byte + + // this should already be validated on startup + if c.config.PrivateKey == nil || len(*c.config.PrivateKey) != 64 { + return errors.New("no valid signing private key provided") + } + + c.Logger.Tracef("signing item for queue %s", channel) + + // sign the item using the private key generated using sign + // + // https://pkg.go.dev/golang.org/x/crypto@v0.1.0/nacl/sign + signed = sign.Sign(out, item, c.config.PrivateKey) + // build a redis queue command to push an item to queue // // https://pkg.go.dev/github.com/go-redis/redis?tab=doc#Client.RPush - pushCmd := c.Redis.RPush(ctx, channel, item) + pushCmd := c.Redis.RPush(ctx, channel, signed) // blocking call to push an item to queue and return err // diff --git a/queue/redis/push_test.go b/queue/redis/push_test.go index 74e815926..e61792ea9 100644 --- a/queue/redis/push_test.go +++ b/queue/redis/push_test.go @@ -28,13 +28,13 @@ func TestRedis_Push(t *testing.T) { } // setup redis mock - _redis, err := NewTest("vela") + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } // setup redis mock - badItem, err := NewTest("vela") + badItem, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } diff --git a/queue/redis/redis.go b/queue/redis/redis.go index 6ed111ec8..4e4f68aea 100644 --- a/queue/redis/redis.go +++ b/queue/redis/redis.go @@ -25,6 +25,10 @@ type config struct { Cluster bool // specifies the timeout to use for the Redis client Timeout time.Duration + // key for signing items pushed to the Redis client + PrivateKey *[64]byte + // key for opening items popped from the Redis client + PublicKey *[32]byte } type client struct { @@ -173,7 +177,7 @@ func pingQueue(c *client) error { // This function is intended for running tests only. // //nolint:revive // ignore returning unexported client -func NewTest(channels ...string) (*client, error) { +func NewTest(signingPrivateKey, signingPublicKey string, channels ...string) (*client, error) { // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run @@ -186,5 +190,7 @@ func NewTest(channels ...string) (*client, error) { WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), WithChannels(channels...), WithCluster(false), + WithPrivateKey(signingPrivateKey), + WithPublicKey(signingPublicKey), ) } diff --git a/queue/redis/redis_test.go b/queue/redis/redis_test.go index aa1bf1a0f..6935a676d 100644 --- a/queue/redis/redis_test.go +++ b/queue/redis/redis_test.go @@ -46,7 +46,9 @@ func Strings(v []string) *[]string { return &v } // setup global variables used for testing. var ( - _build = &library.Build{ + _signingPrivateKey = "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==" + _signingPublicKey = "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=" + _build = &library.Build{ ID: Int64(1), Number: Int(1), Parent: Int(1), diff --git a/queue/redis/route_test.go b/queue/redis/route_test.go index de3f99fc9..23328df43 100644 --- a/queue/redis/route_test.go +++ b/queue/redis/route_test.go @@ -14,7 +14,7 @@ import ( func TestRedis_Client_Route(t *testing.T) { // setup - client, _ := NewTest("vela", "16cpu8gb", "16cpu8gb:gcp", "gcp") + client, _ := NewTest(_signingPrivateKey, _signingPublicKey, "vela", "16cpu8gb", "16cpu8gb:gcp", "gcp") tests := []struct { success bool want string diff --git a/queue/setup.go b/queue/setup.go index ae69a1429..ed0f131c8 100644 --- a/queue/setup.go +++ b/queue/setup.go @@ -30,6 +30,10 @@ type Setup struct { Routes []string // specifies the timeout for pop requests for the queue client Timeout time.Duration + // private key in base64 used for signing items pushed to the queue + PrivateKey string + // public key in base64 used for opening items popped from the queue + PublicKey string } // Redis creates and returns a Vela service capable @@ -45,6 +49,8 @@ func (s *Setup) Redis() (Service, error) { redis.WithChannels(s.Routes...), redis.WithCluster(s.Cluster), redis.WithTimeout(s.Timeout), + redis.WithPrivateKey(s.PrivateKey), + redis.WithPublicKey(s.PublicKey), ) } diff --git a/router/middleware/queue_test.go b/router/middleware/queue_test.go index cfbd94010..80c22ce07 100644 --- a/router/middleware/queue_test.go +++ b/router/middleware/queue_test.go @@ -20,7 +20,8 @@ func TestMiddleware_Queue(t *testing.T) { // setup types var got queue.Service - want, _ := redis.NewTest() + // signing keys are irrelevant here + want, _ := redis.NewTest("", "") // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/signing.go b/router/middleware/signing.go new file mode 100644 index 000000000..05c63b8e5 --- /dev/null +++ b/router/middleware/signing.go @@ -0,0 +1,18 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// QueueSigningPrivateKey is a middleware function that attaches the private key used +// to sign items that are pushed to the queue. +func QueueSigningPrivateKey(key string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("queue.private-key", key) + c.Next() + } +}