support for encrypted secrets

pull/2631/head
Brad Rydzewski 2019-03-18 08:34:36 -07:00
parent bd23f21ff4
commit 504e4754e6
8 changed files with 291 additions and 1 deletions

2
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/drone/drone-go v0.0.0-20190217024616-3e8b71333e59
github.com/drone/drone-runtime v0.0.0-20190210191445-ad403a0ca24e
github.com/drone/drone-ui v0.0.0-20190316194615-9f768293daab
github.com/drone/drone-yaml v1.0.4
github.com/drone/drone-yaml v1.0.5
github.com/drone/envsubst v1.0.1
github.com/drone/go-license v1.0.2
github.com/drone/go-login v1.0.3

9
go.sum
View File

@ -3,7 +3,9 @@ docker.io/go-docker v1.0.0/go.mod h1:7tiAn5a0LFmjbPDbyTPOaTTOuG1ZRNXdPA6RvKY+fpY
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f h1:y2hSFdXeA1y5z5f0vfNO0Dg5qVY036qzlz3Pds0B92o=
github.com/asaskevich/govalidator v0.0.0-20180315120708-ccb8e960c48f/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@ -15,6 +17,7 @@ github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTS
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866 h1:98WJ4YCdjmB7uyrdT3P4A2Oa1hiRPKoa/0zInG6UnfQ=
github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866/go.mod h1:x7AK2h2QzaXVEFi1tbMYMDuvHcCEr1QdMDrg3hkW24Q=
@ -34,6 +37,8 @@ github.com/drone/drone-ui v0.0.0-20190316194615-9f768293daab h1:17/O+A6NUQ76sRy+
github.com/drone/drone-ui v0.0.0-20190316194615-9f768293daab/go.mod h1:NBtVWW7NNJpD9+huMD/5TAE1db2nrEh0i35/9Rf1MPI=
github.com/drone/drone-yaml v1.0.4 h1:NYTEGhf/XJMiJT8CwGy+pMOxWC8C2vhhzEo6/gbT4tU=
github.com/drone/drone-yaml v1.0.4/go.mod h1:eM365p3g9M5sroFBTR/najiGrZnd/GiIpWHC2UW8PoI=
github.com/drone/drone-yaml v1.0.5 h1:LC74aQhyagrBYFTI3XmLEYNkMpEq4QZSQbwiLGyHrsg=
github.com/drone/drone-yaml v1.0.5/go.mod h1:eM365p3g9M5sroFBTR/najiGrZnd/GiIpWHC2UW8PoI=
github.com/drone/envsubst v1.0.1 h1:NOOStingM2sbBwsIUeQkKUz8ShwCUzmqMxWrpXItfPE=
github.com/drone/envsubst v1.0.1/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0=
github.com/drone/go-license v1.0.2 h1:7OwndfYk+Lp/cGHkxe4HUn/Ysrrw3WYH2pnd99yrkok=
@ -59,11 +64,13 @@ github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/gogo/protobuf v0.0.0-20170307180453-100ba4e88506 h1:zDlw+wgyXdfkRuvFCdEDUiPLmZp2cvf/dWHazY0a5VM=
github.com/gogo/protobuf v0.0.0-20170307180453-100ba4e88506/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-jsonnet v0.12.1 h1:v0iUm/b4SBz7lR/diMoz9tLAz8lqtnNRKIwMrmU2HEU=
github.com/google/go-jsonnet v0.12.1/go.mod h1:gVu3UVSfOt5fRFq+dh9duBqXa5905QY8S1QvMNcEIVs=
@ -80,6 +87,7 @@ github.com/gosimple/slug v1.3.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIor
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM=
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/h2non/gock v1.0.10 h1:EzHYzKKSLN4xk0w193uAy3tp8I3+L1jmaI2Mjg4lCgU=
github.com/h2non/gock v1.0.10/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -175,6 +183,7 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuA
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -218,6 +218,7 @@ func (s Server) Handler() http.Handler {
r.Route("/encrypt", func(r chi.Router) {
r.Use(acl.CheckWriteAccess())
r.Post("/", encrypt.Handler(s.Repos))
r.Post("/secret", encrypt.Handler(s.Repos))
})
r.Route("/cron", func(r chi.Router) {

View File

@ -1521,6 +1521,18 @@ func (mr *MockSchedulerMockRecorder) Cancelled(arg0, arg1 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancelled", reflect.TypeOf((*MockScheduler)(nil).Cancelled), arg0, arg1)
}
// Pause mocks base method
func (m *MockScheduler) Pause(arg0 context.Context) error {
ret := m.ctrl.Call(m, "Pause", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Pause indicates an expected call of Pause
func (mr *MockSchedulerMockRecorder) Pause(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockScheduler)(nil).Pause), arg0)
}
// Request mocks base method
func (m *MockScheduler) Request(arg0 context.Context, arg1 core.Filter) (*core.Stage, error) {
ret := m.ctrl.Call(m, "Request", arg0, arg1)
@ -1534,6 +1546,18 @@ func (mr *MockSchedulerMockRecorder) Request(arg0, arg1 interface{}) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Request", reflect.TypeOf((*MockScheduler)(nil).Request), arg0, arg1)
}
// Resume mocks base method
func (m *MockScheduler) Resume(arg0 context.Context) error {
ret := m.ctrl.Call(m, "Resume", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Resume indicates an expected call of Resume
func (mr *MockSchedulerMockRecorder) Resume(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resume", reflect.TypeOf((*MockScheduler)(nil).Resume), arg0)
}
// Schedule mocks base method
func (m *MockScheduler) Schedule(arg0 context.Context, arg1 *core.Stage) error {
ret := m.ctrl.Call(m, "Schedule", arg0, arg1)

View File

@ -236,6 +236,7 @@ func (r *Runner) Run(ctx context.Context, id int64) error {
}
secretService := secret.Combine(
secret.Encrypted(),
secret.Static(m.Secrets),
r.Secrets,
)

View File

@ -0,0 +1,121 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"github.com/drone/drone-yaml/yaml"
"github.com/drone/drone/core"
"github.com/drone/drone/logger"
"github.com/drone/drone/plugin/registry/auths"
)
// Encrypted returns a new encrypted registry credentials
// provider that sournces credentials from the encrypted strings
// in the yaml file.
func Encrypted() core.RegistryService {
return new(encrypted)
}
type encrypted struct {
}
func (c *encrypted) List(ctx context.Context, in *core.RegistryArgs) ([]*core.Registry, error) {
var results []*core.Registry
for _, match := range in.Pipeline.PullSecrets {
logger := logger.FromContext(ctx).
WithField("name", match).
WithField("kind", "secret")
logger.Trace("image_pull_secrets: find encrypted secret")
// lookup the named secret in the manifest. If the
// secret does not exist, return a nil variable,
// allowing the next secret controller in the chain
// to be invoked.
data, ok := getEncrypted(in.Conf, match)
if !ok {
logger.Trace("image_pull_secrets: no matching encrypted secret in yaml")
return nil, nil
}
decoded, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
logger.WithError(err).Trace("image_pull_secrets: cannot decode secret")
return nil, err
}
decrypted, err := decrypt(decoded, []byte(in.Repo.Secret))
if err != nil {
logger.WithError(err).Trace("image_pull_secrets: cannot decrypt secret")
return nil, err
}
parsed, err := auths.ParseBytes(decrypted)
if err != nil {
logger.WithError(err).Trace("image_pull_secrets: cannot parse decrypted secret")
return nil, err
}
logger.Trace("image_pull_secrets: found encrypted secret")
results = append(results, parsed...)
}
return results, nil
}
func getEncrypted(manifest *yaml.Manifest, match string) (data string, ok bool) {
for _, resource := range manifest.Resources {
secret, ok := resource.(*yaml.Secret)
if !ok {
continue
}
if secret.Name != match {
continue
}
if secret.Data == "" {
continue
}
return secret.Data, true
}
return
}
func decrypt(ciphertext []byte, key []byte) (plaintext []byte, err error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(ciphertext) < gcm.NonceSize() {
return nil, errors.New("malformed ciphertext")
}
return gcm.Open(nil,
ciphertext[:gcm.NonceSize()],
ciphertext[gcm.NonceSize():],
nil,
)
}

119
plugin/secret/encrypted.go Normal file
View File

@ -0,0 +1,119 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"github.com/drone/drone-yaml/yaml"
"github.com/drone/drone/core"
"github.com/drone/drone/logger"
)
// Encrypted returns a new encrypted Secret controller.
func Encrypted() core.SecretService {
return new(encrypted)
}
type encrypted struct {
}
func (c *encrypted) Find(ctx context.Context, in *core.SecretArgs) (*core.Secret, error) {
logger := logger.FromContext(ctx).
WithField("name", in.Name).
WithField("kind", "secret")
// lookup the named secret in the manifest. If the
// secret does not exist, return a nil variable,
// allowing the next secret controller in the chain
// to be invoked.
data, ok := getEncrypted(in.Conf, in.Name)
if !ok {
logger.Trace("secret: encrypted: no matching secret")
return nil, nil
}
// if the build event is a pull request and the source
// repository is a fork, the secret is not exposed to
// the pipeline, for security reasons.
if in.Repo.Private &&
in.Build.Event == core.EventPullRequest &&
in.Build.Fork != "" {
logger.Trace("secret: encrypted: restricted from forks")
return nil, nil
}
decoded, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
logger.WithError(err).Trace("secret: encrypted: cannot decode")
return nil, err
}
decrypted, err := decrypt(decoded, []byte(in.Repo.Secret))
if err != nil {
logger.WithError(err).Trace("secret: encrypted: cannot decrypt")
return nil, err
}
logger.Trace("secret: encrypted: found matching secret")
return &core.Secret{
Name: in.Name,
Data: string(decrypted),
}, nil
}
func getEncrypted(manifest *yaml.Manifest, match string) (data string, ok bool) {
for _, resource := range manifest.Resources {
secret, ok := resource.(*yaml.Secret)
if !ok {
continue
}
if secret.Name != match {
continue
}
if secret.Data == "" {
continue
}
return secret.Data, true
}
return
}
func decrypt(ciphertext []byte, key []byte) (plaintext []byte, err error) {
block, err := aes.NewCipher(key[:])
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(ciphertext) < gcm.NonceSize() {
return nil, errors.New("malformed ciphertext")
}
return gcm.Open(nil,
ciphertext[:gcm.NonceSize()],
ciphertext[gcm.NonceSize():],
nil,
)
}

View File

@ -0,0 +1,15 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package secret