Skip to content

Commit 615c647

Browse files
committed
S3CSI-5: Add secret auth for volume creds
This commit implements Kubernetes Secret-based authentication for S3 credentials. Key features include: - Support for "secret" authentication source using nodePublishSecretRef - Proper validation of AWS credential formats using regex patterns - Secure handling with key_id and access_key expected in the Secret - Improved logging that redacts sensitive credentials - Example YAML and documentation for credential-based authentication The implementation follows AWS credential format standards while allowing flexibility for test credentials with different lengths.
1 parent 2843778 commit 615c647

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

pkg/driver/node/credentialprovider/provider.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const (
3333
// We're defaulting to `driver` in this case.
3434
AuthenticationSourceUnspecified AuthenticationSource = ""
3535
AuthenticationSourceDriver AuthenticationSource = "driver"
36-
AuthenticationSourcePod AuthenticationSource = "pod"
36+
AuthenticationSourcePod AuthenticationSource = "pod" // TODO(S3C-9762): Remove other sources of credentials
37+
AuthenticationSourceSecret AuthenticationSource = "secret"
3738
)
3839

3940
// A Provider provides methods for accessing AWS credentials.
@@ -70,6 +71,8 @@ type ProvideContext struct {
7071
StsRegion string
7172
// BucketRegion is the `--region` parameter passed via mount options.
7273
BucketRegion string
74+
// SecretData is a map of key-value pairs from the Kubernetes Secret referenced by nodePublishSecretRef.
75+
SecretData map[string]string
7376
}
7477

7578
// SetWriteAndEnvPath sets `WritePath` and `EnvPath` for `ctx`.
@@ -99,11 +102,14 @@ func (c *Provider) Provide(ctx context.Context, provideCtx ProvideContext) (envp
99102
case AuthenticationSourcePod:
100103
env, err := c.provideFromPod(ctx, provideCtx)
101104
return env, AuthenticationSourcePod, err
105+
case AuthenticationSourceSecret:
106+
env, err := c.provideFromSecret(ctx, provideCtx)
107+
return env, AuthenticationSourceSecret, err
102108
case AuthenticationSourceUnspecified, AuthenticationSourceDriver:
103109
env, err := c.provideFromDriver(provideCtx)
104110
return env, AuthenticationSourceDriver, err
105111
default:
106-
return nil, AuthenticationSourceUnspecified, fmt.Errorf("unknown `authenticationSource`: %s, only `driver` (default option if not specified) and `pod` supported", authenticationSource)
112+
return nil, AuthenticationSourceUnspecified, fmt.Errorf("unknown `authenticationSource`: %s, only `driver` (default option if not specified), `pod`, and `secret` supported", authenticationSource)
107113
}
108114
}
109115

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package credentialprovider
2+
3+
import (
4+
"context"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"unicode/utf8"
9+
10+
"github.com/scality/mountpoint-s3-csi-driver/pkg/driver/node/envprovider"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
13+
"k8s.io/klog/v2"
14+
)
15+
16+
const (
17+
// Keys expected in the Secret map from NodePublishVolumeRequest.
18+
keyID = "key_id"
19+
secretAccessKey = "access_key"
20+
21+
// Upper limits (not exact) — suits Vault & test creds.
22+
maxAccessKeyIDLen = 16
23+
maxSecretAccessKeyLen = 40
24+
)
25+
26+
/*
27+
Validation rules (loosened for cloudserver test credentials):
28+
29+
key_id – 1 … 16 chars, uppercase A–Z or 0–9
30+
access_key – 1 … 40 chars, [A-Za-z0-9 / + =]
31+
32+
The patterns are supersets of AWS IAM and permit shorter dummy keys.
33+
*/
34+
var (
35+
accessKeyIDRe = regexp.MustCompile(`^[A-Z0-9]{1,` + strconv.Itoa(maxAccessKeyIDLen) + `}$`)
36+
secretAccessKeyRe = regexp.MustCompile(`^[A-Za-z0-9/+=]{1,` + strconv.Itoa(maxSecretAccessKeyLen) + `}$`)
37+
)
38+
39+
// provideFromSecret validates credentials from a Kubernetes Secret.
40+
func (c *Provider) provideFromSecret(_ context.Context, provideCtx ProvideContext) (envprovider.Environment, error) {
41+
env := envprovider.Environment{}
42+
43+
valid := map[string]struct{}{keyID: {}, secretAccessKey: {}}
44+
for k := range provideCtx.SecretData {
45+
if _, ok := valid[k]; !ok {
46+
klog.Warningf("credentialprovider: Secret contains unexpected key %q (ignored). Only %q and %q are supported.",
47+
k, keyID, secretAccessKey)
48+
}
49+
}
50+
51+
id, okID := provideCtx.SecretData[keyID]
52+
sec, okSec := provideCtx.SecretData[secretAccessKey]
53+
54+
if okID {
55+
id = strings.TrimSpace(id)
56+
if !accessKeyIDRe.MatchString(id) {
57+
klog.Warningf("credentialprovider: key_id %q is not uppercase alphanumeric or exceeds %d chars",
58+
id, maxAccessKeyIDLen)
59+
okID = false
60+
}
61+
}
62+
63+
if okSec {
64+
sec = strings.TrimSpace(sec)
65+
if !secretAccessKeyRe.MatchString(sec) || !utf8.ValidString(sec) {
66+
klog.Warningf("credentialprovider: access_key is invalid or exceeds %d chars",
67+
maxSecretAccessKeyLen)
68+
okSec = false
69+
}
70+
}
71+
72+
if okID && okSec {
73+
env.Set(envprovider.EnvAccessKeyID, id)
74+
env.Set(envprovider.EnvSecretAccessKey, sec)
75+
76+
// FULL key_id logged (no masking) for audit purposes.
77+
klog.V(3).Infof("credentialprovider: volume %s authenticated with key_id %s",
78+
provideCtx.VolumeID, id)
79+
80+
return env, nil
81+
}
82+
83+
var missing []string
84+
if !okID {
85+
missing = append(missing, keyID)
86+
}
87+
if !okSec {
88+
missing = append(missing, secretAccessKey)
89+
}
90+
return nil, status.Errorf(
91+
codes.InvalidArgument,
92+
"credentialprovider: missing or invalid keys in Kubernetes Secret: %s",
93+
strings.Join(missing, ", "),
94+
)
95+
}

pkg/driver/node/node.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ func credentialProvideContextFromPublishRequest(req *csi.NodePublishVolumeReques
268268
ServiceAccountName: volumeCtx[volumecontext.CSIServiceAccountName],
269269
StsRegion: volumeCtx[volumecontext.STSRegion],
270270
BucketRegion: bucketRegion,
271+
SecretData: req.GetSecrets(),
271272
}
272273
}
273274

@@ -294,6 +295,17 @@ func logSafeNodePublishVolumeRequest(req *csi.NodePublishVolumeRequest) *csi.Nod
294295
safeVolumeContext := maps.Clone(req.VolumeContext)
295296
delete(safeVolumeContext, volumecontext.CSIServiceAccountTokens)
296297

298+
// Create a copy of credential with sensitive values redacted
299+
redactedCredential := make(map[string]string)
300+
for k, v := range req.Secrets {
301+
if k == "access_key" {
302+
// Redact the secret access key
303+
redactedCredential[k] = "[REDACTED]"
304+
} else {
305+
redactedCredential[k] = v
306+
}
307+
}
308+
297309
return &csi.NodePublishVolumeRequest{
298310
VolumeId: req.VolumeId,
299311
PublishContext: req.PublishContext,
@@ -302,5 +314,6 @@ func logSafeNodePublishVolumeRequest(req *csi.NodePublishVolumeRequest) *csi.Nod
302314
VolumeCapability: req.VolumeCapability,
303315
Readonly: req.Readonly,
304316
VolumeContext: safeVolumeContext,
317+
Secrets: redactedCredential,
305318
}
306319
}

0 commit comments

Comments
 (0)