Skip to content

Commit 0859dde

Browse files
committed
kms: add initial bits for plugin lifecycle
1 parent 5221c82 commit 0859dde

6 files changed

Lines changed: 917 additions & 2 deletions

File tree

pkg/operator/encryption/kms/helpers.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ package kms
22

33
import (
44
"fmt"
5+
"path/filepath"
6+
"regexp"
57
"strconv"
68
"strings"
79

10+
configv1 "github.com/openshift/api/config/v1"
811
"github.com/openshift/api/features"
912
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
13+
"github.com/openshift/library-go/pkg/operator/encryption/encoding"
1014
corev1 "k8s.io/api/core/v1"
15+
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
1116
)
1217

18+
var kmsEndpointRegexp = regexp.MustCompile(`^unix:///var/run/kmsplugin/kms-(\d+)\.sock$`)
19+
1320
const providerConfigDataKeyPrefix = "kms-provider-config-"
1421
const credentialDataKeyPrefix = "kms-secret-data-"
22+
const credentialsDir = "/etc/kubernetes/static-pod-resources/secrets/encryption-config"
1523

1624
// ToProviderConfigSecretDataKeyFor constructs the data key for storing a KMS provider config in the encryption-config Secret.
1725
// The keyID must be a valid non-negative integer string.
@@ -113,3 +121,50 @@ func AddKMSPluginVolumeAndMountToPodSpec(podSpec *corev1.PodSpec, containerName
113121

114122
return nil
115123
}
124+
125+
func findFirstKMSConfiguration(config *apiserverv1.EncryptionConfiguration) *apiserverv1.KMSConfiguration {
126+
for _, resource := range config.Resources {
127+
for _, provider := range resource.Providers {
128+
if provider.KMS != nil {
129+
return provider.KMS
130+
}
131+
}
132+
}
133+
return nil
134+
}
135+
136+
func parseKeyIDFromEndpoint(endpoint string) (string, error) {
137+
matches := kmsEndpointRegexp.FindStringSubmatch(endpoint)
138+
if matches == nil {
139+
return "", fmt.Errorf("unexpected KMS endpoint format: %s", endpoint)
140+
}
141+
return matches[1], nil
142+
}
143+
144+
func parseProviderConfig(secret *corev1.Secret, kmsConfiguration *apiserverv1.KMSConfiguration) (*configv1.KMSConfig, error) {
145+
keyID, err := parseKeyIDFromEndpoint(kmsConfiguration.Endpoint)
146+
if err != nil {
147+
return nil, fmt.Errorf("failed to parse key ID from endpoint: %w", err)
148+
}
149+
providerConfigKey, err := ToProviderConfigSecretDataKeyFor(keyID)
150+
if err != nil {
151+
return nil, fmt.Errorf("failed to create provider config secret key ID from endpoint: %w", err)
152+
}
153+
providerConfigData, ok := secret.Data[providerConfigKey]
154+
if !ok {
155+
return nil, fmt.Errorf("missing provider config key %s in encryption-config secret", providerConfigKey)
156+
}
157+
kmsConfig, err := encoding.DecodeKMSConfig(providerConfigData)
158+
if err != nil {
159+
return nil, fmt.Errorf("failed to decode provider config: %w", err)
160+
}
161+
return kmsConfig, nil
162+
}
163+
164+
func parseSecretDataPath(kmsConfiguration *apiserverv1.KMSConfiguration) (string, error) {
165+
keyID, err := parseKeyIDFromEndpoint(kmsConfiguration.Endpoint)
166+
if err != nil {
167+
return "", fmt.Errorf("failed to parse key ID from endpoint: %w", err)
168+
}
169+
return filepath.Join(credentialsDir, credentialDataKeyPrefix+keyID), nil
170+
}

pkg/operator/encryption/kms/helpers_test.go

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
configv1 "github.com/openshift/api/config/v1"
88
"github.com/openshift/api/features"
99
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
10-
corev1 "k8s.io/api/core/v1"
11-
10+
"github.com/openshift/library-go/pkg/operator/encryption/encoding"
1211
"github.com/stretchr/testify/require"
12+
corev1 "k8s.io/api/core/v1"
13+
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
1314
)
1415

1516
func TestKeyIDFromProviderConfigSecretDataKey(t *testing.T) {
@@ -114,6 +115,145 @@ func TestToProviderConfigSecretDataKeyFor(t *testing.T) {
114115
}
115116
}
116117

118+
func TestParseKeyIDFromEndpoint(t *testing.T) {
119+
tests := []struct {
120+
name string
121+
endpoint string
122+
wantKeyID string
123+
wantErr string
124+
}{
125+
{
126+
name: "standard endpoint",
127+
endpoint: "unix:///var/run/kmsplugin/kms-555.sock",
128+
wantKeyID: "555",
129+
},
130+
{
131+
name: "single digit key ID",
132+
endpoint: "unix:///var/run/kmsplugin/kms-3.sock",
133+
wantKeyID: "3",
134+
},
135+
{
136+
name: "missing kms- prefix",
137+
endpoint: "unix:///var/run/kmsplugin/plugin-555.sock",
138+
wantErr: "unexpected KMS endpoint format",
139+
},
140+
{
141+
name: "missing .sock suffix",
142+
endpoint: "unix:///var/run/kmsplugin/kms-555.socket",
143+
wantErr: "unexpected KMS endpoint format",
144+
},
145+
{
146+
name: "empty key ID",
147+
endpoint: "unix:///var/run/kmsplugin/kms-.sock",
148+
wantErr: "unexpected KMS endpoint format",
149+
},
150+
{
151+
name: "no unix prefix",
152+
endpoint: "/var/run/kmsplugin/kms-555.sock",
153+
wantErr: "unexpected KMS endpoint format",
154+
},
155+
{
156+
name: "no digit key ID",
157+
endpoint: "/var/run/kmsplugin/kms-abc.sock",
158+
wantErr: "unexpected KMS endpoint format",
159+
},
160+
}
161+
162+
for _, tt := range tests {
163+
t.Run(tt.name, func(t *testing.T) {
164+
keyID, err := parseKeyIDFromEndpoint(tt.endpoint)
165+
if tt.wantErr != "" {
166+
require.ErrorContains(t, err, tt.wantErr)
167+
return
168+
}
169+
require.NoError(t, err)
170+
require.Equal(t, tt.wantKeyID, keyID)
171+
})
172+
}
173+
}
174+
175+
func TestParseProviderConfig(t *testing.T) {
176+
vaultConfig := &configv1.KMSConfig{
177+
Type: configv1.VaultKMSProvider,
178+
Vault: configv1.VaultKMSConfig{
179+
KMSPluginImage: "quay.io/test/vault:v1",
180+
VaultAddress: "https://vault.example.com:8200",
181+
VaultNamespace: "my-namespace",
182+
TransitKey: "my-key",
183+
TransitMount: "transit",
184+
},
185+
}
186+
configBytes, err := encoding.EncodeKMSConfig(vaultConfig)
187+
require.NoError(t, err)
188+
189+
providerConfigKey, err := ToProviderConfigSecretDataKeyFor("555")
190+
require.NoError(t, err)
191+
192+
tests := []struct {
193+
name string
194+
secret *corev1.Secret
195+
kmsConfig *apiserverv1.KMSConfiguration
196+
wantErr string
197+
}{
198+
{
199+
name: "valid provider config",
200+
secret: &corev1.Secret{
201+
Data: map[string][]byte{
202+
providerConfigKey: configBytes,
203+
},
204+
},
205+
kmsConfig: &apiserverv1.KMSConfiguration{
206+
Endpoint: "unix:///var/run/kmsplugin/kms-555.sock",
207+
},
208+
},
209+
{
210+
name: "missing provider config key",
211+
secret: &corev1.Secret{
212+
Data: map[string][]byte{},
213+
},
214+
kmsConfig: &apiserverv1.KMSConfiguration{
215+
Endpoint: "unix:///var/run/kmsplugin/kms-555.sock",
216+
},
217+
wantErr: "missing provider config key",
218+
},
219+
{
220+
name: "invalid JSON",
221+
secret: &corev1.Secret{
222+
Data: map[string][]byte{
223+
providerConfigKey: []byte(`{invalid`),
224+
},
225+
},
226+
kmsConfig: &apiserverv1.KMSConfiguration{
227+
Endpoint: "unix:///var/run/kmsplugin/kms-555.sock",
228+
},
229+
wantErr: "failed to decode provider config",
230+
},
231+
{
232+
name: "invalid endpoint",
233+
secret: &corev1.Secret{
234+
Data: map[string][]byte{},
235+
},
236+
kmsConfig: &apiserverv1.KMSConfiguration{
237+
Endpoint: "invalid-endpoint",
238+
},
239+
wantErr: "failed to parse key ID from endpoint",
240+
},
241+
}
242+
243+
for _, tt := range tests {
244+
t.Run(tt.name, func(t *testing.T) {
245+
config, err := parseProviderConfig(tt.secret, tt.kmsConfig)
246+
if tt.wantErr != "" {
247+
require.ErrorContains(t, err, tt.wantErr)
248+
return
249+
}
250+
require.NoError(t, err)
251+
require.NotNil(t, config)
252+
require.Equal(t, vaultConfig, config)
253+
})
254+
}
255+
}
256+
117257
func TestAddKMSPluginVolume(t *testing.T) {
118258
directoryOrCreate := corev1.HostPathDirectoryOrCreate
119259

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package plugins
2+
3+
import (
4+
"fmt"
5+
6+
configv1 "github.com/openshift/api/config/v1"
7+
corev1 "k8s.io/api/core/v1"
8+
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
9+
"k8s.io/utils/ptr"
10+
)
11+
12+
type VaultSidecarProvider struct {
13+
Config *configv1.VaultKMSConfig
14+
SecretDataPath string
15+
}
16+
17+
func NewVaultSidecarProvider(providerConfig *configv1.KMSConfig, secretDataPath string) (*VaultSidecarProvider, error) {
18+
return &VaultSidecarProvider{
19+
Config: &providerConfig.Vault,
20+
SecretDataPath: secretDataPath,
21+
}, nil
22+
}
23+
24+
func (v *VaultSidecarProvider) BuildSidecarContainer(name string, kmsConfig *apiserverv1.KMSConfiguration) (corev1.Container, error) {
25+
if v.Config == nil {
26+
return corev1.Container{}, fmt.Errorf("vault config cannot be nil")
27+
}
28+
29+
// FIXME: this is fragile. TBD
30+
args := fmt.Sprintf(`set -e
31+
CREDS=$(cat %s)
32+
SECRET_ID=${CREDS#*\"VAULT_SECRET_ID\":\"}
33+
SECRET_ID=${SECRET_ID%%%%\"*}
34+
ROLE_ID=${CREDS#*\"VAULT_ROLE_ID\":\"}
35+
ROLE_ID=${ROLE_ID%%%%\"*}
36+
printf '%%s' "$SECRET_ID" > /tmp/secret-id
37+
exec /vault-kube-kms \
38+
-listen-address=%s \
39+
-vault-address=%s \
40+
-vault-namespace=%s \
41+
-transit-mount=%s \
42+
-transit-key=%s \
43+
-approle-role-id=$ROLE_ID \
44+
-approle-secret-id-path=/tmp/secret-id`,
45+
v.SecretDataPath,
46+
kmsConfig.Endpoint,
47+
v.Config.VaultAddress,
48+
v.Config.VaultNamespace,
49+
v.Config.TransitMount,
50+
v.Config.TransitKey,
51+
)
52+
53+
return corev1.Container{
54+
Name: name,
55+
Image: v.Config.KMSPluginImage,
56+
Command: []string{"/bin/sh", "-c"},
57+
Args: []string{args},
58+
// This is necessary to read the secret data in /etc/kubernetes/static-pod-resources/secrets/encryption-config
59+
SecurityContext: &corev1.SecurityContext{
60+
RunAsUser: ptr.To(int64(0)),
61+
},
62+
}, nil
63+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package plugins
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
configv1 "github.com/openshift/api/config/v1"
8+
"github.com/stretchr/testify/require"
9+
apiserverv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
10+
"k8s.io/utils/ptr"
11+
)
12+
13+
func TestVaultSidecarProvider_BuildSidecarContainer(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
vaultConfig *configv1.VaultKMSConfig
17+
secretDataPath string
18+
containerName string
19+
kmsConfig *apiserverv1.KMSConfiguration
20+
wantErr string
21+
}{
22+
{
23+
name: "builds container with correct args",
24+
secretDataPath: "/etc/kubernetes/static-pod-resources/secrets/encryption-config/kms-secret-data-555",
25+
vaultConfig: &configv1.VaultKMSConfig{
26+
KMSPluginImage: "quay.io/test/vault:v2",
27+
VaultAddress: "https://vault.example.com:8200",
28+
VaultNamespace: "my-namespace",
29+
TransitKey: "my-key",
30+
TransitMount: "transit",
31+
},
32+
containerName: "kms-plugin",
33+
kmsConfig: &apiserverv1.KMSConfiguration{
34+
APIVersion: "v2",
35+
Name: "555_secrets",
36+
Endpoint: "unix:///var/run/kmsplugin/kms-555.sock",
37+
},
38+
},
39+
{
40+
name: "nil vault config",
41+
secretDataPath: "/etc/kubernetes/static-pod-resources/secrets/encryption-config/kms-secret-data-555",
42+
vaultConfig: nil,
43+
containerName: "kms-plugin",
44+
kmsConfig: &apiserverv1.KMSConfiguration{},
45+
wantErr: "vault config cannot be nil",
46+
},
47+
}
48+
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
provider := &VaultSidecarProvider{
52+
Config: tt.vaultConfig,
53+
SecretDataPath: tt.secretDataPath,
54+
}
55+
56+
container, err := provider.BuildSidecarContainer(tt.containerName, tt.kmsConfig)
57+
if tt.wantErr != "" {
58+
require.ErrorContains(t, err, tt.wantErr)
59+
return
60+
}
61+
require.NoError(t, err)
62+
require.Equal(t, tt.containerName, container.Name)
63+
require.Equal(t, tt.vaultConfig.KMSPluginImage, container.Image)
64+
require.Equal(t, []string{"/bin/sh", "-c"}, container.Command)
65+
66+
expectedArgs := fmt.Sprintf(`set -e
67+
CREDS=$(cat %s)
68+
SECRET_ID=${CREDS#*\"VAULT_SECRET_ID\":\"}
69+
SECRET_ID=${SECRET_ID%%%%\"*}
70+
ROLE_ID=${CREDS#*\"VAULT_ROLE_ID\":\"}
71+
ROLE_ID=${ROLE_ID%%%%\"*}
72+
printf '%%s' "$SECRET_ID" > /tmp/secret-id
73+
exec /vault-kube-kms \
74+
-listen-address=%s \
75+
-vault-address=%s \
76+
-vault-namespace=%s \
77+
-transit-mount=%s \
78+
-transit-key=%s \
79+
-approle-role-id=$ROLE_ID \
80+
-approle-secret-id-path=/tmp/secret-id`,
81+
tt.secretDataPath,
82+
tt.kmsConfig.Endpoint,
83+
tt.vaultConfig.VaultAddress,
84+
tt.vaultConfig.VaultNamespace,
85+
tt.vaultConfig.TransitMount,
86+
tt.vaultConfig.TransitKey,
87+
)
88+
require.Len(t, container.Args, 1)
89+
require.Equal(t, expectedArgs, container.Args[0])
90+
require.Equal(t, ptr.To(int64(0)), container.SecurityContext.RunAsUser)
91+
})
92+
}
93+
}

0 commit comments

Comments
 (0)