forked from fcantournet/kubernetes-flexvolume-vault-plugin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
220 lines (182 loc) · 6.27 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package main
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"syscall"
"github.com/fcantournet/kubernetes-flexvolume-vault-plugin/flexvolume"
cleanhttp "github.com/hashicorp/go-cleanhttp"
vaultapi "github.com/hashicorp/vault/api"
"github.com/kelseyhightower/envconfig"
)
// vaultSecretFlexVolume implement the flexvolume interface
// the struct tags are for envconfig
type vaultSecretFlexVolume struct {
Address string `default:"https://vault.service:8200"`
ServerName string `default:"vault.service"`
GeneratorTokenPath string `default:"/etc/kubernetes/vaulttoken"`
TokenWrappTTL string `default:"5m"`
TokenFilename string `default:"vault-token"`
}
// VaultTmpfsOptions is the struct that should be unmarshaled from the json send by the kubelet
// Corresponds to the arbitrary payload that we can specify to the kubelet to send
// in the yaml defining the pod/deployment
type VaultTmpfsOptions struct {
Policies []string `json:"vault/policies"`
}
func (v vaultSecretFlexVolume) NewOptions() interface{} {
return &VaultTmpfsOptions{}
}
// Init is a no-op here but necessary to satisfy the interface
func (v vaultSecretFlexVolume) Init() flexvolume.Response {
return flexvolume.Succeed()
}
// Attach is not necessary for this plugin but need to be implemented to satisfy the interface
func (v vaultSecretFlexVolume) Attach(arg interface{}) flexvolume.Response {
return flexvolume.Succeed()
}
// Detach is not necessary for this plugin but need to be implemented to satisfy the interface
func (v vaultSecretFlexVolume) Detach(arg string) flexvolume.Response {
return flexvolume.Succeed()
}
// Mount create the tmpfs volume and mounts it @ dir
func (v vaultSecretFlexVolume) Mount(dir string, dev string, opts interface{}) flexvolume.Response {
opt := opts.(*VaultTmpfsOptions) // casting because golang sucks
if len(opt.Policies) == 0 {
return flexvolume.Fail(fmt.Sprintf("Missing policies under %v in %v:", "vault/policies", opts))
}
wrappedToken, err := v.getTokenForPolicy(opt.Policies)
if err != nil {
return flexvolume.Fail(fmt.Sprintf("Couldn't obtain wrapped token (for policies %v): %v", opt.Policies, err))
}
err = insertWrappedTokenInVolume(wrappedToken, dir, v.TokenFilename)
if err != nil {
return flexvolume.Fail(fmt.Sprintf("Couldn't create secret volume: %v", err))
}
return flexvolume.Succeed()
}
// Unmount unmounts the volume ( and delete the tmpfs ?)
func (v vaultSecretFlexVolume) Unmount(dir string) flexvolume.Response {
err := syscall.Unmount(dir, 0)
if err != nil {
return flexvolume.Fail(fmt.Sprintf("Failed to Unmount %v: %v", dir, err))
}
return flexvolume.Succeed(fmt.Sprintf("Unmounted: %v", dir))
}
// Get a wrapped token from Vault scoped with given policy
func (v vaultSecretFlexVolume) getTokenForPolicy(policies []string) (*vaultapi.SecretWrapInfo, error) {
client, err := v.createVaultClient()
if err != nil {
return nil, fmt.Errorf("Couldn't create vault client: %v", err)
}
req := vaultapi.TokenCreateRequest{
Policies: policies,
}
wrapped, err := client.Auth().Token().Create(&req)
if err != nil {
return nil, fmt.Errorf("Couldn't create scoped token for policy %v : %v", req.Policies, err)
}
return wrapped.WrapInfo, nil
}
func insertWrappedTokenInVolume(wrapped *vaultapi.SecretWrapInfo, dir string, tokenfilename string) error {
err := os.MkdirAll(dir, 0755)
if err != nil {
return fmt.Errorf("Failed to mkdir %v: %v", dir, err)
}
if err = mountVaultTmpFsAt(dir); err != nil {
return err
}
tokenpath := path.Join(dir, tokenfilename)
fulljsonpath := path.Join(dir, tokenfilename, ".json")
fulljson, err := json.Marshal(wrapped)
if err != nil {
return fmt.Errorf("Couldn't marshal vault response: %v", err)
}
err = ioutil.WriteFile(tokenpath, []byte(strings.TrimSpace(wrapped.Token)), 0644)
if err != nil {
return err
}
err = os.Chmod(tokenpath, 0644)
if err != nil {
return err
}
err = ioutil.WriteFile(fulljsonpath, fulljson, 0644)
if err != nil {
return err
}
err = os.Chmod(fulljsonpath, 0644)
return err
}
// mountVaultTmpFsAt mounts a tmpfs filesystem at the given path
// this doesn't take care of setting the permission on the path.
func mountVaultTmpFsAt(dir string) error {
var flags uintptr
flags = syscall.MS_NOATIME | syscall.MS_SILENT
flags |= syscall.MS_NODEV | syscall.MS_NOEXEC | syscall.MS_NOSUID
options := "size=1M"
err := syscall.Mount("tmpfs", dir, "tmpfs", flags, options)
return os.NewSyscallError("mount", err)
}
func tokenFromFile(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
data = bytes.TrimRight(data, "\n")
return string(data), nil
}
func (v vaultSecretFlexVolume) createVaultClient() (*vaultapi.Client, error) {
// this is a global var in vault pkg
vaultapi.DefaultWrappingTTL = v.TokenWrappTTL
// Get token with token generator policy
token, err := tokenFromFile(v.GeneratorTokenPath)
if err != nil {
return nil, fmt.Errorf("Couldn't read generator token from file %v: %v", v.GeneratorTokenPath, err)
}
// Generate the default config
vaultConfig := vaultapi.DefaultConfig()
if v.Address == "" {
return nil, fmt.Errorf("missing vault address")
}
vaultConfig.Address = v.Address
var tlsConfig tls.Config
tlsConfig.RootCAs, err = x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("Failed to get system CAs: %v", err)
}
tlsConfig.BuildNameToCertificate()
// SSL verification
if v.ServerName == "" {
return nil, fmt.Errorf("missing vault TLS server host name")
}
tlsConfig.ServerName = v.ServerName
tlsConfig.InsecureSkipVerify = false
transport := cleanhttp.DefaultTransport()
transport.TLSClientConfig = &tlsConfig
// Setup the new transport
vaultConfig.HttpClient.Transport = transport
// Create the client
client, err := vaultapi.NewClient(vaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to create vault cient: %s", err)
}
client.SetToken(token)
// The generator token is periodic so we can set the increment to 0
// and it will default to the period.
client.Auth().Token().RenewSelf(0)
return client, nil
}
func main() {
var vf vaultSecretFlexVolume
err := envconfig.Process("VAULTTMPFS", &vf)
if err != nil {
flexvolume.Fail(fmt.Sprintf("Failed to init configuration: %v", err))
}
flexvolume.RunPlugin(vf)
}