Skip to content

Commit a0ff5bc

Browse files
InverseIntegralMarcosDY
authored andcommitted
Support K8s auth (spiffe#5058)
Signed-off-by: Matteo Kamm <[email protected]>
1 parent 2b4b9c1 commit a0ff5bc

File tree

5 files changed

+223
-3
lines changed

5 files changed

+223
-3
lines changed

pkg/server/plugin/keymanager/hashicorpvault/hashicorp_vault.go

+34-3
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,21 @@ type Config struct {
5555
Namespace string `hcl:"namespace" json:"namespace"`
5656
// TransitEnginePath specifies the path to the transit engine to perform key operations.
5757
TransitEnginePath string `hcl:"transit_engine_path" json:"transit_engine_path"`
58+
5859
// If true, vault client accepts any server certificates.
5960
// It should be used only test environment so on.
6061
InsecureSkipVerify bool `hcl:"insecure_skip_verify" json:"insecure_skip_verify"`
6162

63+
// TODO: Support CA certificate path here instead of insecure skip verify
64+
6265
// Configuration for the Token authentication method
6366
TokenAuth *TokenAuthConfig `hcl:"token_auth" json:"token_auth,omitempty"`
6467
// Configuration for the AppRole authentication method
6568
AppRoleAuth *AppRoleAuthConfig `hcl:"approle_auth" json:"approle_auth,omitempty"`
6669
// Configuration for the Client Certificate authentication method
6770
CertAuth *CertAuthConfig `hcl:"cert_auth" json:"cert_auth,omitempty"`
68-
69-
// TODO: Support other auth methods
70-
// TODO: Support client certificate and key
71+
// Configuration for the Kubernetes authentication method
72+
K8sAuth *K8sAuthConfig `hcl:"k8s_auth" json:"k8s_auth,omitempty"`
7173
}
7274

7375
// TokenAuthConfig represents parameters for token auth method
@@ -103,6 +105,18 @@ type CertAuthConfig struct {
103105
ClientKeyPath string `hcl:"client_key_path" json:"client_key_path"`
104106
}
105107

108+
// K8sAuthConfig represents parameters for Kubernetes auth method.
109+
type K8sAuthConfig struct {
110+
// Name of the mount point where Kubernetes auth method is mounted. (e.g., /auth/<mount_point>/login)
111+
// If the value is empty, use default mount point (/auth/kubernetes)
112+
K8sAuthMountPoint string `hcl:"k8s_auth_mount_point" json:"k8s_auth_mount_point"`
113+
// Name of the Vault role.
114+
// The plugin authenticates against the named role.
115+
K8sAuthRoleName string `hcl:"k8s_auth_role_name" json:"k8s_auth_role_name"`
116+
// Path to the Kubernetes Service Account Token to use authentication with the Vault.
117+
TokenPath string `hcl:"token_path" json:"token_path"`
118+
}
119+
106120
// Plugin is the main representation of this keymanager plugin
107121
type Plugin struct {
108122
keymanagerv1.UnsafeKeyManagerServer
@@ -188,6 +202,13 @@ func parseAuthMethod(config *Config) (AuthMethod, error) {
188202
authMethod = CERT
189203
}
190204

205+
if config.K8sAuth != nil {
206+
if err := checkForAuthMethodConfigured(authMethod); err != nil {
207+
return 0, err
208+
}
209+
authMethod = K8S
210+
}
211+
191212
if authMethod != 0 {
192213
return authMethod, nil
193214
}
@@ -222,6 +243,16 @@ func (p *Plugin) genClientParams(method AuthMethod, config *Config) (*ClientPara
222243
cp.CertAuthRoleName = config.CertAuth.CertAuthRoleName
223244
cp.ClientCertPath = p.getEnvOrDefault(envVaultClientCert, config.CertAuth.ClientCertPath)
224245
cp.ClientKeyPath = p.getEnvOrDefault(envVaultClientKey, config.CertAuth.ClientKeyPath)
246+
case K8S:
247+
if config.K8sAuth.K8sAuthRoleName == "" {
248+
return nil, status.Error(codes.InvalidArgument, "k8s_auth_role_name is required")
249+
}
250+
if config.K8sAuth.TokenPath == "" {
251+
return nil, status.Error(codes.InvalidArgument, "token_path is required")
252+
}
253+
cp.K8sAuthMountPoint = config.K8sAuth.K8sAuthMountPoint
254+
cp.K8sAuthRoleName = config.K8sAuth.K8sAuthRoleName
255+
cp.K8sAuthTokenPath = config.K8sAuth.TokenPath
225256
}
226257

227258
return cp, nil
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEnwIBAAKCAQAwrCHZ8ldBNltOjJTUMWopdAuHGcxuPUsTjdaoZL71q6YC8TbD
3+
cD5aFX152g17tfSHbukr53YD+0TfrDcL/vdSt7Acs5FUHK1ULcuzGvhXx2rUiosW
4+
Zk8Nc99gjwHXOV3DoUBVk04edXo7SMmVKPiYemwm0XvSoBhU3NpnBGJ/DQq7TG+W
5+
wFIaxbURpVxpUP2oWZRebUuQgund8Pjh6kxUkX6XcFH+0y4+wMDV3YdLTuFTYwEc
6+
q/XqdUIEasc1lPT7CwwAlxR+jQTKGnDji6KQerSiktwOUjBpQVb/j/m2+53suhju
7+
XHLUcId2x9yfe73kTTMcYsQ4woEHt9xGRniJAgMBAAECggEAKC5y49LFZfjR+E7m
8+
ryb8VayPt8D8nCXNzR7Tj8FcRMSoENXCOCZ50zTambYCW5cjgIt3w98Z9r+BZIZw
9+
C186Hve2VHuKBr6F+XC1Me+aBh2DfGPD34Im0RxP1Q86ncumNNLyobMyUsL5XegB
10+
QzrHwFmQ35shdgjlDWomg9WC2w/Y2P6zLpbua/lZNBBo3ISXdU1EZNdCl6cJct5N
11+
Q9bbr6PJrL1JdQIC8fA0c1MXiN5XCAaVqSuxlLqiTVrNcTPweJb4iHbvgNf7pvwT
12+
kPEH/10dQirdtjFPR8+WmihES8lIWBcqqemB/dpDoLpjyTo3ZcLODKQiE4o0gLyu
13+
Mw+C4QKBgHOXDRwH/7rxif4S+ngxcJS1OKigfHbf0JLdEVX6X6y5QkGZAd9c3hgP
14+
FbroBx0XXohrLaXcaDVAx8DpylPum2NuibsTg/HUToc9FxH5PXYYp15Thjai2KJ0
15+
zMjV/Z3DuzJ8465Cv0QL7kuCalgilEU01F03zVaIgnm1ZBjZyAtfAoGAa8u/x6C4
16+
9VNdYSgIhDzPPmTaxWy6jWFZV5mcmRckHqYGQFw8c9VFCFA07endetbn+3SniDi5
17+
ujnNV+HStLTHq5uv1QkqCWFXc8B06vKcfLbwsHCzPOcRz7NHGfICQpHKo64R+/un
18+
RWJv1KO1u0gvMy/4/OJXDYFn2YsZ5CFKbRcCgYBXXIav9Ou24u8kdDuRs+weuIjG
19+
CeWIAsik9ygvDzhYVvxYj8f2hT3meSA3Tz5xIkR0Xmz1uouYFAnlJ82fees/T0AR
20+
gEJs98USOX3CO9nT8/YrOH1rtdB9mEFeWT2Bi3lkQzfhcNkWGN5Ve4/cZOYjGDaY
21+
7Z/oEuxqCEpK7e5fiQKBgAqQ9kODJZ4mhci4O916eHYNPMSNW9vv5uoHTKpU8l1u
22+
uL4mTGauSQ3/jrCjc+pOln63eJSJuureL5qlsBm2frv7jsi7FTvGJuRZwRwmm+A9
23+
rmodIfSeUciiMh4A8ufDkrFopqqkiEjs1Tlqsq2g7b9+vFFNfmr8fEl+sRMDkGAR
24+
AoGACfGod8qGIMX8gxRiLGPVK98wJAJhlLxeVztoSP3pQbpyf+GU7r+Nf8DyzhE5
25+
xomJ1aF25lBMSGSo74QZgIpFxcNKbR+9zcYpzsSJfq9vIktvgLYPn9g7GDr1rWMi
26+
r8G7GT0udgiJIODc7JFGuBDid4iwlHQZdCFmot3gfBbpsJk=
27+
-----END RSA PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik1ZWTNhcmZRaWVTRzlrR3Y0NE5JSEpmWHB6aUswVFRibFBQQ3ZjN1ZFX0UifQ.eyJhdWQiOlsiYXBpIiwic3BpcmUtc2VydmVyIl0sImV4cCI6NDgxMDQxMzg1MywiaWF0IjoxNjIzMjA0MjUzLCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMiLCJrdWJlcm5ldGVzLmlvIjp7Im5hbWVzcGFjZSI6InNwaXJlIiwicG9kIjp7Im5hbWUiOiJzcGlyZS1zZXJ2ZXItMCIsInVpZCI6ImY5MDIzNzAyLWY0ZWQtNGVkOS1hYjQwLWJjNjkxNDJhYTlhNiJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoic3BpcmUtc2VydmVyIiwidWlkIjoiNjgwOGI0YzctMGI1My00NWY0LTgzZjctZTg5Mzc3NTZlZWFlIn19LCJuYmYiOjE2MjMyMDQyNTMsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpzcGlyZTpzcGlyZS1zZXJ2ZXIifQ.G_dFjt8NzCFq-_QRm8Kbvq4Lt2iJN7Eos57k82aj2dS4TEMkefc2D07MLG4Sur3f2TYZ0xt51Cp3tCKaH8trUyS7sM07_gPO1GLtj-sAKgiRSjrbLPh2Du_J7Rapb42CN77Nb9EhZcc-B1zSg-J56Ypnl54M4UDotbYxIdHEHNvVWQf4KPP2X2IX47b_7Osm1p1jE3p086F6xSA3iDTIIpa6c1Ch3EzjXPK7XgdEDaVpI0TyrO2r2wBeVDTXSO0E8GWzSnaMnAPzypmdSK7jhD0bpF1SClLTC7PCbkqF6K9C-dQM0F-QWoM1hPMTJGG5bQy_xtQS6PT_b-uPUYNpzA

pkg/server/plugin/keymanager/hashicorpvault/vault_client_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,112 @@ func TestNewAuthenticatedClientCertAuthFailed(t *testing.T) {
330330
spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/cert/login: Error making API request.")
331331
}
332332

333+
func TestNewAuthenticatedClientK8sAuth(t *testing.T) {
334+
fakeVaultServer := newFakeVaultServer()
335+
fakeVaultServer.K8sAuthResponseCode = 200
336+
for _, tt := range []struct {
337+
name string
338+
response []byte
339+
renew bool
340+
namespace string
341+
}{
342+
{
343+
name: "K8s Authentication success / Token is renewable",
344+
response: []byte(testK8sAuthResponse),
345+
renew: true,
346+
},
347+
{
348+
name: "K8s Authentication success / Token is not renewable",
349+
response: []byte(testK8sAuthResponseNotRenewable),
350+
},
351+
{
352+
name: "K8s Authentication success / Token is renewable / Namespace is given",
353+
response: []byte(testK8sAuthResponse),
354+
renew: true,
355+
namespace: "test-ns",
356+
},
357+
} {
358+
tt := tt
359+
t.Run(tt.name, func(t *testing.T) {
360+
fakeVaultServer.K8sAuthResponse = tt.response
361+
362+
s, addr, err := fakeVaultServer.NewTLSServer()
363+
require.NoError(t, err)
364+
365+
s.Start()
366+
defer s.Close()
367+
368+
cp := &ClientParams{
369+
VaultAddr: fmt.Sprintf("https://%v/", addr),
370+
Namespace: tt.namespace,
371+
CACertPath: testRootCert,
372+
K8sAuthRoleName: "my-role",
373+
K8sAuthTokenPath: "testdata/k8s/token",
374+
}
375+
cc, err := NewClientConfig(cp, hclog.Default())
376+
require.NoError(t, err)
377+
378+
renewCh := make(chan struct{})
379+
client, err := cc.NewAuthenticatedClient(K8S, renewCh)
380+
require.NoError(t, err)
381+
382+
select {
383+
case <-renewCh:
384+
require.Equal(t, false, tt.renew)
385+
default:
386+
require.Equal(t, true, tt.renew)
387+
}
388+
389+
if cp.Namespace != "" {
390+
headers := client.vaultClient.Headers()
391+
require.Equal(t, cp.Namespace, headers.Get(consts.NamespaceHeaderName))
392+
}
393+
})
394+
}
395+
}
396+
397+
func TestNewAuthenticatedClientK8sAuthFailed(t *testing.T) {
398+
fakeVaultServer := newFakeVaultServer()
399+
fakeVaultServer.K8sAuthResponseCode = 500
400+
401+
s, addr, err := fakeVaultServer.NewTLSServer()
402+
require.NoError(t, err)
403+
404+
s.Start()
405+
defer s.Close()
406+
407+
retry := 0 // Disable retry
408+
cp := &ClientParams{
409+
MaxRetries: &retry,
410+
VaultAddr: fmt.Sprintf("https://%v/", addr),
411+
CACertPath: testRootCert,
412+
K8sAuthRoleName: "my-role",
413+
K8sAuthTokenPath: "testdata/k8s/token",
414+
}
415+
cc, err := NewClientConfig(cp, hclog.Default())
416+
require.NoError(t, err)
417+
418+
renewCh := make(chan struct{})
419+
_, err = cc.NewAuthenticatedClient(K8S, renewCh)
420+
spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Unauthenticated, "authentication failed auth/kubernetes/login: Error making API request.")
421+
}
422+
423+
func TestNewAuthenticatedClientK8sAuthInvalidPath(t *testing.T) {
424+
retry := 0 // Disable retry
425+
cp := &ClientParams{
426+
MaxRetries: &retry,
427+
VaultAddr: "https://example.org:8200",
428+
CACertPath: testRootCert,
429+
K8sAuthTokenPath: "invalid/k8s/token",
430+
}
431+
cc, err := NewClientConfig(cp, hclog.Default())
432+
require.NoError(t, err)
433+
434+
renewCh := make(chan struct{})
435+
_, err = cc.NewAuthenticatedClient(K8S, renewCh)
436+
spiretest.RequireGRPCStatusHasPrefix(t, err, codes.Internal, "failed to read k8s service account token:")
437+
}
438+
333439
func TestRenewTokenFailed(t *testing.T) {
334440
fakeVaultServer := newFakeVaultServer()
335441
fakeVaultServer.LookupSelfResponse = []byte(testLookupSelfResponseShortTTL)

pkg/server/plugin/keymanager/hashicorpvault/vault_fake_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,61 @@ var (
211211
"renewable": false
212212
}
213213
}`
214+
215+
testK8sAuthResponse = `{
216+
"lease_id": "",
217+
"renewable": false,
218+
"lease_duration": 0,
219+
"data": null,
220+
"wrap_info": null,
221+
"warnings": null,
222+
"auth": {
223+
"client_token": "s.scngmDktKCWVRhkggMiyV7E7",
224+
"accessor": "",
225+
"policies": ["default"],
226+
"token_policies": ["default"],
227+
"metadata": {
228+
"role": "my-role",
229+
"service_account_name": "spire-server",
230+
"service_account_namespace": "spire",
231+
"service_account_secret_name": "",
232+
"service_account_uid": "6808b4c7-0b53-45f4-83f7-e8937756eeae"
233+
},
234+
"lease_duration": 3600,
235+
"renewable": true,
236+
"entity_id": "c69a6e0e-3f2c-98a0-39f9-e4d3d7cc294f",
237+
"token_type": "service",
238+
"orphan": true
239+
}
240+
}
241+
`
242+
243+
testK8sAuthResponseNotRenewable = `{
244+
"lease_id": "",
245+
"renewable": false,
246+
"lease_duration": 0,
247+
"data": null,
248+
"wrap_info": null,
249+
"warnings": null,
250+
"auth": {
251+
"client_token": "b.AAAAAQIUprvfquccAKnvL....",
252+
"accessor": "",
253+
"policies": ["default"],
254+
"token_policies": ["default"],
255+
"metadata": {
256+
"role": "my-role",
257+
"service_account_name": "spire-server",
258+
"service_account_namespace": "spire",
259+
"service_account_secret_name": "",
260+
"service_account_uid": "6808b4c7-0b53-45f4-83f7-e8937756eeae"
261+
},
262+
"lease_duration": 3600,
263+
"renewable": false,
264+
"entity_id": "c69a6e0e-3f2c-98a0-39f9-e4d3d7cc294f",
265+
"token_type": "batch",
266+
"orphan": true
267+
}
268+
}`
214269
)
215270

216271
type FakeVaultServerConfig struct {

0 commit comments

Comments
 (0)