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

fix(operator): Improve validation of provided S3 storage configuration #12181

Merged
merged 28 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f58f781
validate s3 endpoint
btaani Mar 12, 2024
387c876
fix lint and unit tests
btaani Mar 12, 2024
4592405
modify CHANGELOG.md
btaani Mar 12, 2024
43f3ef8
Merge branch 'main' into s3-secret-validation
btaani Mar 12, 2024
aff3958
add check on non-AWS urls
btaani Mar 12, 2024
0eb6af9
Merge branch 'main' into s3-secret-validation
btaani Mar 15, 2024
4372a52
refactor validation function and add more descriptive error messages
btaani Mar 15, 2024
322db80
Merge branch 'main' into s3-secret-validation
btaani Mar 15, 2024
498854e
Merge branch 'main' into s3-secret-validation
btaani Mar 25, 2024
d8dfb55
Merge branch 'main' into s3-secret-validation
btaani Mar 26, 2024
5761162
Update operator/internal/handlers/internal/storage/secrets.go
btaani Mar 26, 2024
e5054e7
Update operator/internal/handlers/internal/storage/secrets.go
btaani Mar 26, 2024
34c2ee1
Update operator/internal/handlers/internal/storage/secrets.go
btaani Mar 26, 2024
120e0ac
Update operator/internal/handlers/internal/storage/secrets.go
btaani Mar 26, 2024
fd25913
Update operator/internal/handlers/internal/storage/secrets.go
btaani Mar 26, 2024
c4f565e
address comments
btaani Mar 26, 2024
b162ed1
Merge branch 'main' into s3-secret-validation
btaani Mar 26, 2024
85736b2
Merge branch 'main' into s3-secret-validation
btaani Mar 26, 2024
96f3abf
Merge branch 'main' into s3-secret-validation
btaani Mar 27, 2024
e1c9d93
Merge branch 'main' into s3-secret-validation
btaani Apr 2, 2024
04b43cd
move validation to static mode only
btaani Apr 2, 2024
37255a8
Merge branch 'main' into s3-secret-validation
btaani Apr 2, 2024
1074cdd
Merge branch 'main' into s3-secret-validation
btaani Apr 3, 2024
99c0cc7
Update operator/internal/handlers/internal/storage/secrets.go
btaani Apr 3, 2024
3849bcb
fix indentation
btaani Apr 3, 2024
da85761
Merge branch 'main' into s3-secret-validation
btaani Apr 4, 2024
0aec339
Clearer error message for leaving out schema
xperimental Apr 3, 2024
0e4c74f
Merge branch 'main' into s3-secret-validation
btaani Apr 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions operator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Main

- [12181](https://github.com/grafana/loki/pull/12181) **btaani**: Improve validation of provided S3 storage configuration
- [12333](https://github.com/grafana/loki/pull/12333) **periklis**: Bump max OpenShift version to next release

## 0.6.0 (2024-03-19)
Expand Down
38 changes: 36 additions & 2 deletions operator/internal/handlers/internal/storage/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"errors"
"fmt"
"io"
"net/url"
"sort"
"strings"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -38,6 +40,10 @@ var (
errAzureInvalidEnvironment = errors.New("azure environment invalid (valid values: AzureGlobal, AzureChinaCloud, AzureGermanCloud, AzureUSGovernment)")
errAzureInvalidAccountKey = errors.New("azure account key is not valid base64")

errS3EndpointUnparseable = errors.New("s3 endpoint is not parseable")
errS3EndpointUnsupportedScheme = errors.New("scheme of S3 endpoint URL is unsupported")
errS3EndpointAWSInvalid = errors.New("endpoint for AWS S3 must include correct region")

errGCPParseCredentialsFile = errors.New("gcp storage secret cannot be parsed from JSON content")
errGCPWrongCredentialSourceFile = errors.New("credential source in secret needs to point to token file")

Expand All @@ -49,7 +55,10 @@ var (
}
)

const gcpAccountTypeExternal = "external_account"
const (
awsEndpointSuffix = ".amazonaws.com"
gcpAccountTypeExternal = "external_account"
)

func getSecrets(ctx context.Context, k k8s.Client, stack *lokiv1.LokiStack, fg configv1.FeatureGates) (*corev1.Secret, *corev1.Secret, error) {
var (
Expand Down Expand Up @@ -408,6 +417,8 @@ func extractS3ConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMod
SSE: sseCfg,
}

err = validateS3Endpoint(string(endpoint), string(region))

switch credentialMode {
case lokiv1.CredentialModeTokenCCO:
cfg.STS = true
Expand All @@ -416,17 +427,21 @@ func extractS3ConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMod
if len(roleArn) != 0 {
return nil, fmt.Errorf("%w: %s", errSecretFieldNotAllowed, storage.KeyAWSRoleArn)
}

// In the STS case region is not an optional field
if len(region) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAWSRegion)
} else if len(endpoint) != 0 && err != nil {
btaani marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}

return cfg, nil
case lokiv1.CredentialModeStatic:
cfg.Endpoint = string(endpoint)

if len(endpoint) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAWSEndpoint)
} else if len(region) != 0 && err != nil {
return nil, err
}
if len(id) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAWSAccessKeyID)
Expand All @@ -443,13 +458,32 @@ func extractS3ConfigSecret(s *corev1.Secret, credentialMode lokiv1.CredentialMod
// In the STS case region is not an optional field
if len(region) == 0 {
return nil, fmt.Errorf("%w: %s", errSecretMissingField, storage.KeyAWSRegion)
} else if len(endpoint) != 0 && err != nil {
return nil, err
}
return cfg, nil
default:
return nil, fmt.Errorf("%w: %s", errSecretUnknownCredentialMode, credentialMode)
}
}

func validateS3Endpoint(endpoint string, region string) error {
parsedURL, err := url.Parse(endpoint)
if err != nil {
return fmt.Errorf("%w: %w", errS3EndpointUnparseable, err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("%w: %s", errS3EndpointUnsupportedScheme, parsedURL.Scheme)
}
if strings.HasSuffix(endpoint, awsEndpointSuffix) {
validEndpoint := fmt.Sprintf("https://s3.%s%s", region, awsEndpointSuffix)
if endpoint != validEndpoint {
return fmt.Errorf("%w: %s", errS3EndpointAWSInvalid, validEndpoint)
}
}
return nil
}

func extractS3SSEConfig(d map[string][]byte) (storage.S3SSEConfig, error) {
var (
sseType storage.S3SSEType
Expand Down
58 changes: 50 additions & 8 deletions operator/internal/handlers/internal/storage/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ func TestS3Extract(t *testing.T) {
name: "missing access_key_id",
secret: &corev1.Secret{
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
},
},
Expand All @@ -402,7 +402,7 @@ func TestS3Extract(t *testing.T) {
name: "missing access_key_secret",
secret: &corev1.Secret{
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
},
Expand All @@ -413,7 +413,7 @@ func TestS3Extract(t *testing.T) {
name: "unsupported SSE type",
secret: &corev1.Secret{
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand All @@ -426,7 +426,7 @@ func TestS3Extract(t *testing.T) {
name: "missing SSE-KMS kms_key_id",
secret: &corev1.Secret{
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand All @@ -441,7 +441,7 @@ func TestS3Extract(t *testing.T) {
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand All @@ -456,7 +456,7 @@ func TestS3Extract(t *testing.T) {
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand All @@ -472,7 +472,7 @@ func TestS3Extract(t *testing.T) {
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand All @@ -486,7 +486,7 @@ func TestS3Extract(t *testing.T) {
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("here"),
"endpoint": []byte("https://s3.REGION.amazonaws.com"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
Expand Down Expand Up @@ -530,6 +530,48 @@ func TestS3Extract(t *testing.T) {
},
wantCredentialMode: lokiv1.CredentialModeToken,
},
{
name: "endpoint url missing http/https",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("invalid"),
"region": []byte("region"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
},
},
wantError: "scheme of S3 endpoint URL is unsupported: ",
},
{
name: "s3 region used in endpoint URL is incorrect",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("http://s3.wrong.amazonaws.com"),
"region": []byte("region"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
},
},
wantError: "endpoint for AWS S3 must include correct region: https://s3.region.amazonaws.com",
},
{
name: "s3 endpoint format is not a valid s3 URL",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Data: map[string][]byte{
"endpoint": []byte("http://region.amazonaws.com"),
"region": []byte("region"),
"bucketnames": []byte("this,that"),
"access_key_id": []byte("id"),
"access_key_secret": []byte("secret"),
},
},
wantError: "endpoint for AWS S3 must include correct region: https://s3.region.amazonaws.com",
},
}
for _, tst := range table {
tst := tst
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var (
Namespace: "some-ns",
},
Data: map[string][]byte{
"endpoint": []byte("s3://your-endpoint"),
"endpoint": []byte("https://s3.a-region.amazonaws.com"),
"region": []byte("a-region"),
"bucketnames": []byte("bucket1,bucket2"),
"access_key_id": []byte("a-secret-id"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ var (
Namespace: "some-ns",
},
Data: map[string][]byte{
"endpoint": []byte("s3://your-endpoint"),
"endpoint": []byte("https://s3.a-region.amazonaws.com"),
"region": []byte("a-region"),
"bucketnames": []byte("bucket1,bucket2"),
"access_key_id": []byte("a-secret-id"),
Expand Down
Loading