Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate S3 server credentials on-the-fly #3114

Merged
merged 1 commit into from Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Generate S3 server credentials on-the-fly
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
commit 75268c4d2c8f7e4db22c63d76b451ba6545d117f
23 changes: 19 additions & 4 deletions cmd/gitops/beta/run/cmd.go
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
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
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
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
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
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
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
@@ -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
@@ -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))
})
}
}