Skip to content

Commit

Permalink
Generate S3 server credentials on-the-fly
Browse files Browse the repository at this point in the history
To improve the security of GitOps Run we now generate access and
secret keys each time the `gitops beta run` command is run. These
credentials are passed on to the S3 server and used in the client code
for authentication.
  • Loading branch information
opudrovs authored and makkes committed Dec 8, 2022
1 parent 5871fc3 commit 75268c4
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 18 deletions.
23 changes: 19 additions & 4 deletions cmd/gitops/beta/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,16 +623,29 @@ func runCommandWithoutSession(cmd *cobra.Command, args []string) error {
devBucketHTTPPort := unusedPorts[0]
devBucketHTTPSPort := unusedPorts[1]

cancelDevBucketPortForwarding, cert, err := watch.InstallDevBucketServer(ctx, log0, kubeClient, cfg, devBucketHTTPPort, devBucketHTTPSPort)
// generate access key and secret key for Minio auth
accessKey, err := s3.GenerateAccessKey(s3.DefaultRandIntFunc)
if err != nil {
cancel()
return fmt.Errorf("failed generating access key: %w", err)
}

secretKey, err := s3.GenerateSecretKey(s3.DefaultRandIntFunc)
if err != nil {
cancel()
return fmt.Errorf("failed generating secret key: %w", err)
}

cancelDevBucketPortForwarding, cert, err := watch.InstallDevBucketServer(ctx, log0, kubeClient, cfg, devBucketHTTPPort, devBucketHTTPSPort, accessKey, secretKey)
if err != nil {
cancel()
return fmt.Errorf("unable to install S3 bucket server: %w", err)
}

log, err := logger.NewS3LogWriter(sessionName, fmt.Sprintf("localhost:%d", devBucketHTTPSPort), cert, log0)
log, err := logger.NewS3LogWriter(sessionName, fmt.Sprintf("localhost:%d", devBucketHTTPSPort), accessKey, secretKey, cert, log0)
if err != nil {
cancel()
return err
return fmt.Errorf("failed creating S3 log writer: %w", err)
}

// ====================== Dashboard ======================
Expand Down Expand Up @@ -669,6 +682,8 @@ func runCommandWithoutSession(cmd *cobra.Command, args []string) error {
DevBucketPort: devBucketHTTPPort,
SessionName: sessionName,
Username: username,
AccessKey: accessKey,
SecretKey: secretKey,
}

if !isHelm(paths.GetAbsoluteTargetDir()) {
Expand All @@ -683,7 +698,7 @@ func runCommandWithoutSession(cmd *cobra.Command, args []string) error {
}
}

minioClient, err := s3.NewMinioClient("localhost:"+strconv.Itoa(int(devBucketHTTPSPort)), cert)
minioClient, err := s3.NewMinioClient("localhost:"+strconv.Itoa(int(devBucketHTTPSPort)), accessKey, secretKey, cert)
if err != nil {
cancel()
return err
Expand Down
4 changes: 2 additions & 2 deletions pkg/logger/s3_log_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ func (l *S3LogWriter) L() logr.Logger {
return l.log0.L()
}

func NewS3LogWriter(id, endpoint string, caCert []byte, log0 Logger) (Logger, error) {
minioClient, err := s3.NewMinioClient(endpoint, caCert)
func NewS3LogWriter(id, endpoint string, accessKey, secretKey, caCert []byte, log0 Logger) (Logger, error) {
minioClient, err := s3.NewMinioClient(endpoint, accessKey, secretKey, caCert)
if err != nil {
return nil, err
}
Expand Down
39 changes: 36 additions & 3 deletions pkg/run/watch/install_dev_bucket_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ var (
)

// InstallDevBucketServer installs the dev bucket server, open port forwarding, and returns a function that can be used to the port forwarding.
func InstallDevBucketServer(ctx context.Context, log logger.Logger, kubeClient client.Client, config *rest.Config, httpPort, httpsPort int32) (func(), []byte, error) {
func InstallDevBucketServer(
ctx context.Context,
log logger.Logger,
kubeClient client.Client,
config *rest.Config,
httpPort,
httpsPort int32,
accessKey,
secretKey []byte) (func(), []byte, error) {
var (
err error
devBucketAppLabels = map[string]string{
Expand Down Expand Up @@ -106,6 +114,21 @@ func InstallDevBucketServer(ctx context.Context, log logger.Logger, kubeClient c
log.Successf("Service %s/%s already existed", GitOpsRunNamespace, RunDevBucketName)
}

credentialsSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: GitOpsRunNamespace,
Name: fmt.Sprintf("%s-credentials", RunDevBucketName),
},
Data: map[string][]byte{
"accesskey": accessKey,
"secretkey": secretKey,
},
}
if err := kubeClient.Create(ctx, &credentialsSecret); err != nil {
log.Failuref("Error creating credentials secret: %s", err.Error())
return nil, nil, fmt.Errorf("failed creating credentials secret: %w", err)
}

cert, err := tls.GenerateSelfSignedCertificate("localhost", fmt.Sprintf("%s.%s.svc.cluster.local", devBucketService.Name, devBucketService.Namespace))
if err != nil {
err = fmt.Errorf("failed generating self-signed certificate for dev bucket server: %w", err)
Expand Down Expand Up @@ -162,8 +185,18 @@ func InstallDevBucketServer(ctx context.Context, log logger.Logger, kubeClient c
Image: DevBucketContainerImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Env: []corev1.EnvVar{
{Name: "MINIO_ROOT_USER", Value: "user"},
{Name: "MINIO_ROOT_PASSWORD", Value: "doesn't matter"},
{Name: "MINIO_ROOT_USER", ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: credentialsSecret.Name},
Key: "accesskey",
},
}},
{Name: "MINIO_ROOT_PASSWORD", ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: credentialsSecret.Name},
Key: "secretkey",
},
}},
},
Ports: []corev1.ContainerPort{
{
Expand Down
5 changes: 3 additions & 2 deletions pkg/run/watch/setup_bucket_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import (
func createBucketAndSecretObjects(params SetupRunObjectParams) (corev1.Secret, sourcev1.Bucket) {
var devBucketCredentials = fmt.Sprintf("%s-credentials", RunDevBucketName)

// create a secret
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: devBucketCredentials,
Namespace: params.Namespace,
},
Data: map[string][]byte{
"accesskey": []byte("user"),
"secretkey": []byte("doesn't matter"),
"accesskey": params.AccessKey,
"secretkey": params.SecretKey,
},
Type: "Opaque",
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/run/watch/setup_dev_ks.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type SetupRunObjectParams struct {
DevBucketPort int32
SessionName string
Username string
AccessKey []byte
SecretKey []byte
}

func SetupBucketSourceAndKS(ctx context.Context, log logger.Logger, kubeClient client.Client, params SetupRunObjectParams) error {
Expand Down
19 changes: 14 additions & 5 deletions pkg/s3/auth_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ func TestVerifySignature(t *testing.T) {
expected error
}

g := NewGomegaWithT(t)

accessKey, err := GenerateAccessKey(DefaultRandIntFunc)
g.Expect(err).NotTo(HaveOccurred(), "failed generating access key")

secretKey, err := GenerateSecretKey(DefaultRandIntFunc)
g.Expect(err).NotTo(HaveOccurred(), "failed generating secret key")

for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
for _, region := range []string{"us-east-1", "us-west-1"} {
for _, host := range []string{"", "localhost", "localhost:8080", "localhost:9000"} {
Expand All @@ -52,7 +60,8 @@ func TestVerifySignature(t *testing.T) {
if err != nil {
t.Fatal(err)
}
signed := signer.SignV4(*req, "gitopsrun", "gitopsrun123", "", region)

signed := signer.SignV4(*req, string(accessKey), string(secretKey), "", region)
return signed
}(),
expected: nil,
Expand All @@ -70,7 +79,7 @@ func TestVerifySignature(t *testing.T) {
if err != nil {
t.Fatal(err)
}
signed := signer.SignV4(*req, "gitopsrun", "invalid", "", region)
signed := signer.SignV4(*req, string(accessKey), "invalid", "", region)
return signed
}(),
expected: fmt.Errorf("access denied: signature does not match"),
Expand All @@ -88,7 +97,7 @@ func TestVerifySignature(t *testing.T) {
if err != nil {
t.Fatal(err)
}
signed := signer.SignV4(*req, "invalid", "gitopsrun123", "", region)
signed := signer.SignV4(*req, "invalid", string(secretKey), "", region)
return signed
}(),
expected: fmt.Errorf("access denied: credential does not match"),
Expand All @@ -104,9 +113,9 @@ func TestVerifySignature(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
g := NewGomegaWithT(t)
if tc.expected == nil {
g.Expect(verifySignature(*tc.req, "gitopsrun", "gitopsrun123")).To(Succeed())
g.Expect(verifySignature(*tc.req, string(accessKey), string(secretKey))).To(Succeed())
} else {
g.Expect(verifySignature(*tc.req, "gitopsrun", "gitopsrun123").Error()).To(Equal(tc.expected.Error()))
g.Expect(verifySignature(*tc.req, string(accessKey), string(secretKey)).Error()).To(Equal(tc.expected.Error()))
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/s3/minio.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials"
)

func NewMinioClient(endpoint string, caCert []byte) (*minio.Client, error) {
func NewMinioClient(endpoint string, accessKey, secretKey, caCert []byte) (*minio.Client, error) {
tr, err := NewTLSRoundTripper(caCert)
if err != nil {
return nil, fmt.Errorf("failed creating transport: %w", err)
Expand All @@ -19,7 +19,7 @@ func NewMinioClient(endpoint string, caCert []byte) (*minio.Client, error) {
return minio.New(
endpoint,
&minio.Options{
Creds: credentials.NewStaticV4("user", "doesn't matter", ""),
Creds: credentials.NewStaticV4(string(accessKey), string(secretKey), ""),
Secure: true,
BucketLookup: minio.BucketLookupPath,
Transport: tr,
Expand Down
50 changes: 50 additions & 0 deletions pkg/s3/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package s3

import (
"crypto/rand"
"fmt"
"io"
"math/big"
)

type RandIntFunc func(io.Reader, *big.Int) (*big.Int, error)

var DefaultRandIntFunc = rand.Int

const (
numRandomCharsInAccessKey = 10
numRandomCharsInSecretKey = 40
)

var (
accessKeyLetters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
secretKeyLetters = []rune("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
)

func GenerateAccessKey(randInt RandIntFunc) ([]byte, error) {
accessKey, err := generateRandomKey(randInt, numRandomCharsInAccessKey, accessKeyLetters)
if err != nil {
return nil, err
}

return []byte("AKIA" + string(accessKey)), nil
}

func GenerateSecretKey(randInt RandIntFunc) ([]byte, error) {
return generateRandomKey(randInt, numRandomCharsInSecretKey, secretKeyLetters)
}

func generateRandomKey(randInt RandIntFunc, numChars int, letters []rune) ([]byte, error) {
key := make([]rune, numChars)

for i := range key {
num, err := randInt(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return nil, fmt.Errorf("failed to get random number: %w", err)
}

key[i] = letters[num.Int64()]
}

return []byte(string(key)), nil
}
82 changes: 82 additions & 0 deletions pkg/s3/secret_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package s3

import (
"fmt"
"io"
"math/big"
"math/rand"
"testing"

. "github.com/onsi/gomega"
)

func deterministicRandInt(seed int64, err error) RandIntFunc {
seeded := false

return func(_ io.Reader, max *big.Int) (*big.Int, error) {
if err != nil {
return nil, err
}

if !seeded {
rand.Seed(seed)

seeded = true
}

return big.NewInt(int64(rand.Intn(int(max.Int64())))), nil
}
}

func TestGenerators(t *testing.T) {
tests := []struct {
name string
generator func(RandIntFunc) ([]byte, error)
randIntFunc RandIntFunc
expected string
expectedErr bool
}{
{
name: "GenerateAccessKey generates a deterministic access key",
generator: GenerateAccessKey,
randIntFunc: deterministicRandInt(100, nil),
expected: "AKIA5UQA4UZJM3",
expectedErr: false,
},
{
name: "GenerateAccessKey properly returns an error if RNG fails",
generator: GenerateAccessKey,
randIntFunc: deterministicRandInt(0, fmt.Errorf("foobar")),
expected: "",
expectedErr: true,
},
{
name: "GenerateSecretKey generates a deterministic secret key",
generator: GenerateSecretKey,
randIntFunc: deterministicRandInt(512, nil),
expected: "Fg5n9W6CwTfnMu4FzEk8xuTomwk2OpFe0yLcLMAL",
expectedErr: false,
},
{
name: "GenerateSecretKey properly returns an error if RNG fails",
generator: GenerateSecretKey,
randIntFunc: deterministicRandInt(0, fmt.Errorf("foobar")),
expected: "",
expectedErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGomegaWithT(t)

accessKey, err := tt.generator(tt.randIntFunc)
if tt.expectedErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).NotTo(HaveOccurred())
}
g.Expect(string(accessKey)).To(Equal(tt.expected))
})
}
}

0 comments on commit 75268c4

Please sign in to comment.