Skip to content

Commit 02fd0e7

Browse files
authored
Merge pull request smallstep#913 from delamart/master
Vault Kubernetes Auth
2 parents 3c4d041 + 07984a9 commit 02fd0e7

File tree

9 files changed

+543
-257
lines changed

9 files changed

+543
-257
lines changed

cas/vaultcas/auth/approle/approle.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package approle
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/hashicorp/vault/api/auth/approle"
9+
)
10+
11+
// AuthOptions defines the configuration options added using the
12+
// VaultOptions.AuthOptions field when AuthType is approle
13+
type AuthOptions struct {
14+
RoleID string `json:"roleID,omitempty"`
15+
SecretID string `json:"secretID,omitempty"`
16+
SecretIDFile string `json:"secretIDFile,omitempty"`
17+
SecretIDEnv string `json:"secretIDEnv,omitempty"`
18+
IsWrappingToken bool `json:"isWrappingToken,omitempty"`
19+
}
20+
21+
func NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.AppRoleAuth, error) {
22+
var opts *AuthOptions
23+
24+
err := json.Unmarshal(options, &opts)
25+
if err != nil {
26+
return nil, fmt.Errorf("error decoding AppRole auth options: %w", err)
27+
}
28+
29+
var approleAuth *approle.AppRoleAuth
30+
31+
var loginOptions []approle.LoginOption
32+
if mountPath != "" {
33+
loginOptions = append(loginOptions, approle.WithMountPath(mountPath))
34+
}
35+
if opts.IsWrappingToken {
36+
loginOptions = append(loginOptions, approle.WithWrappingToken())
37+
}
38+
39+
if opts.RoleID == "" {
40+
return nil, errors.New("you must set roleID")
41+
}
42+
43+
var sid approle.SecretID
44+
switch {
45+
case opts.SecretID != "" && opts.SecretIDFile == "" && opts.SecretIDEnv == "":
46+
sid = approle.SecretID{
47+
FromString: opts.SecretID,
48+
}
49+
case opts.SecretIDFile != "" && opts.SecretID == "" && opts.SecretIDEnv == "":
50+
sid = approle.SecretID{
51+
FromFile: opts.SecretIDFile,
52+
}
53+
case opts.SecretIDEnv != "" && opts.SecretIDFile == "" && opts.SecretID == "":
54+
sid = approle.SecretID{
55+
FromEnv: opts.SecretIDEnv,
56+
}
57+
default:
58+
return nil, errors.New("you must set one of secretID, secretIDFile or secretIDEnv")
59+
}
60+
61+
approleAuth, err = approle.NewAppRoleAuth(opts.RoleID, &sid, loginOptions...)
62+
if err != nil {
63+
return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err)
64+
}
65+
66+
return approleAuth, nil
67+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package approle
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"net/url"
10+
"testing"
11+
12+
vault "github.com/hashicorp/vault/api"
13+
)
14+
15+
func testCAHelper(t *testing.T) (*url.URL, *vault.Client) {
16+
t.Helper()
17+
18+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
switch {
20+
case r.RequestURI == "/v1/auth/approle/login":
21+
w.WriteHeader(http.StatusOK)
22+
fmt.Fprintf(w, `{
23+
"auth": {
24+
"client_token": "hvs.0000"
25+
}
26+
}`)
27+
case r.RequestURI == "/v1/auth/custom-approle/login":
28+
w.WriteHeader(http.StatusOK)
29+
fmt.Fprintf(w, `{
30+
"auth": {
31+
"client_token": "hvs.9999"
32+
}
33+
}`)
34+
default:
35+
w.WriteHeader(http.StatusNotFound)
36+
fmt.Fprintf(w, `{"error":"not found"}`)
37+
}
38+
}))
39+
t.Cleanup(func() {
40+
srv.Close()
41+
})
42+
u, err := url.Parse(srv.URL)
43+
if err != nil {
44+
srv.Close()
45+
t.Fatal(err)
46+
}
47+
48+
config := vault.DefaultConfig()
49+
config.Address = srv.URL
50+
51+
client, err := vault.NewClient(config)
52+
if err != nil {
53+
srv.Close()
54+
t.Fatal(err)
55+
}
56+
57+
return u, client
58+
}
59+
60+
func TestApprole_LoginMountPaths(t *testing.T) {
61+
caURL, _ := testCAHelper(t)
62+
63+
config := vault.DefaultConfig()
64+
config.Address = caURL.String()
65+
client, _ := vault.NewClient(config)
66+
67+
tests := []struct {
68+
name string
69+
mountPath string
70+
token string
71+
}{
72+
{
73+
name: "ok default mount path",
74+
mountPath: "",
75+
token: "hvs.0000",
76+
},
77+
{
78+
name: "ok explicit mount path",
79+
mountPath: "approle",
80+
token: "hvs.0000",
81+
},
82+
{
83+
name: "ok custom mount path",
84+
mountPath: "custom-approle",
85+
token: "hvs.9999",
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
method, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(`{"RoleID":"roleID","SecretID":"secretID","IsWrappingToken":false}`))
92+
if err != nil {
93+
t.Errorf("NewApproleAuthMethod() error = %v", err)
94+
return
95+
}
96+
97+
secret, err := client.Auth().Login(context.Background(), method)
98+
if err != nil {
99+
t.Errorf("Login() error = %v", err)
100+
return
101+
}
102+
103+
token, _ := secret.TokenID()
104+
if token != tt.token {
105+
t.Errorf("Token error got %v, expected %v", token, tt.token)
106+
return
107+
}
108+
})
109+
}
110+
}
111+
112+
func TestApprole_NewApproleAuthMethod(t *testing.T) {
113+
tests := []struct {
114+
name string
115+
mountPath string
116+
raw string
117+
wantErr bool
118+
}{
119+
{
120+
"ok secret-id string",
121+
"",
122+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000"}`,
123+
false,
124+
},
125+
{
126+
"ok secret-id string and wrapped",
127+
"",
128+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`,
129+
false,
130+
},
131+
{
132+
"ok secret-id string and wrapped with custom mountPath",
133+
"approle2",
134+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "isWrappedToken": true}`,
135+
false,
136+
},
137+
{
138+
"ok secret-id file",
139+
"",
140+
`{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`,
141+
false,
142+
},
143+
{
144+
"ok secret-id env",
145+
"",
146+
`{"RoleID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
147+
false,
148+
},
149+
{
150+
"fail mandatory role-id",
151+
"",
152+
`{}`,
153+
true,
154+
},
155+
{
156+
"fail mandatory secret-id any",
157+
"",
158+
`{"RoleID": "0000-0000-0000-0000"}`,
159+
true,
160+
},
161+
{
162+
"fail multiple secret-id types id and env",
163+
"",
164+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
165+
true,
166+
},
167+
{
168+
"fail multiple secret-id types id and file",
169+
"",
170+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id"}`,
171+
true,
172+
},
173+
{
174+
"fail multiple secret-id types env and file",
175+
"",
176+
`{"RoleID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
177+
true,
178+
},
179+
{
180+
"fail multiple secret-id types all",
181+
"",
182+
`{"RoleID": "0000-0000-0000-0000", "SecretID": "0000-0000-0000-0000", "SecretIDFile": "./secret-id", "SecretIDEnv": "VAULT_APPROLE_SECRETID"}`,
183+
true,
184+
},
185+
}
186+
for _, tt := range tests {
187+
t.Run(tt.name, func(t *testing.T) {
188+
_, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(tt.raw))
189+
if (err != nil) != tt.wantErr {
190+
t.Errorf("Approle.NewApproleAuthMethod() error = %v, wantErr %v", err, tt.wantErr)
191+
return
192+
}
193+
})
194+
}
195+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package kubernetes
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/hashicorp/vault/api/auth/kubernetes"
9+
)
10+
11+
// AuthOptions defines the configuration options added using the
12+
// VaultOptions.AuthOptions field when AuthType is kubernetes
13+
type AuthOptions struct {
14+
Role string `json:"role,omitempty"`
15+
TokenPath string `json:"tokenPath,omitempty"`
16+
}
17+
18+
func NewKubernetesAuthMethod(mountPath string, options json.RawMessage) (*kubernetes.KubernetesAuth, error) {
19+
var opts *AuthOptions
20+
21+
err := json.Unmarshal(options, &opts)
22+
if err != nil {
23+
return nil, fmt.Errorf("error decoding Kubernetes auth options: %w", err)
24+
}
25+
26+
var kubernetesAuth *kubernetes.KubernetesAuth
27+
28+
var loginOptions []kubernetes.LoginOption
29+
if mountPath != "" {
30+
loginOptions = append(loginOptions, kubernetes.WithMountPath(mountPath))
31+
}
32+
if opts.TokenPath != "" {
33+
loginOptions = append(loginOptions, kubernetes.WithServiceAccountTokenPath(opts.TokenPath))
34+
}
35+
36+
if opts.Role == "" {
37+
return nil, errors.New("you must set role")
38+
}
39+
40+
kubernetesAuth, err = kubernetes.NewKubernetesAuth(
41+
opts.Role,
42+
loginOptions...,
43+
)
44+
if err != nil {
45+
return nil, fmt.Errorf("unable to initialize Kubernetes auth method: %w", err)
46+
}
47+
48+
return kubernetesAuth, nil
49+
}

0 commit comments

Comments
 (0)