-
Notifications
You must be signed in to change notification settings - Fork 42
/
provider.go
267 lines (223 loc) · 8.58 KB
/
provider.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package conjur
import (
"errors"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/cenkalti/backoff"
"github.com/cyberark/conjur-api-go/conjurapi"
"github.com/cyberark/conjur-api-go/conjurapi/authn"
"github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator"
authnConfig "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config"
plugin_v1 "github.com/cyberark/secretless-broker/internal/plugin/v1"
)
const authenticatorTokenFile = "/run/conjur/access-token"
// Provider provides data values from the Conjur vault.
type Provider struct {
// Data related to Provider config
AuthenticationMutex *sync.Mutex
Authenticator *authenticator.Authenticator
Config conjurapi.Config
Conjur *conjurapi.Client
Version string
Name string
// Credentials for API-key based auth
APIKey string
Username string
// Authn URL for K8s-authenticator based auth
AuthnURL string
}
func hasField(field string, params *map[string]string) (ok bool) {
_, ok = (*params)[field]
return
}
// ProviderFactory constructs a Conjur Provider. The API client is configured from
// environment variables.
// To authenticate with Conjur, you can provide Secretless with:
// - A Conjur username and API key
// - A path to a file where a Conjur access token is stored
// - Config info to use the Conjur k8s authenticator client to retrieve an access token
// from Conjur (i.e. Conjur version, account, authn url, username, and SSL cert)
func ProviderFactory(options plugin_v1.ProviderOptions) (plugin_v1.Provider, error) {
config, err := conjurapi.LoadConfig()
if err != nil {
return nil, fmt.Errorf("ERROR: Conjur provider could not load configuration: %s", err)
}
var apiKey, authnURL, tokenFile, username, version string
var authenticator *authenticator.Authenticator
var conjur *conjurapi.Client
var provider *Provider
authenticationMutex := &sync.Mutex{}
username = os.Getenv("CONJUR_AUTHN_LOGIN")
apiKey = os.Getenv("CONJUR_AUTHN_API_KEY")
tokenFile = os.Getenv("CONJUR_AUTHN_TOKEN_FILE")
authnURL = os.Getenv("CONJUR_AUTHN_URL")
version = os.Getenv("CONJUR_VERSION")
if len(version) == 0 {
version = "5"
}
provider = &Provider{
Name: options.Name,
Config: config,
Username: username,
APIKey: apiKey,
AuthnURL: authnURL,
AuthenticationMutex: authenticationMutex,
Version: version,
}
if provider.Username != "" && provider.APIKey != "" {
log.Printf("Info: Conjur provider using API key-based authentication")
if conjur, err = conjurapi.NewClientFromKey(provider.Config, authn.LoginPair{provider.Username, provider.APIKey}); err != nil {
return nil, fmt.Errorf("ERROR: Could not create new Conjur provider: %s", err)
}
} else if tokenFile != "" {
log.Printf("Info: Conjur provider using access token-based authentication")
if conjur, err = conjurapi.NewClientFromTokenFile(provider.Config, tokenFile); err != nil {
return nil, fmt.Errorf("ERROR: Could not create new Conjur provider: %s", err)
}
} else if provider.AuthnURL != "" && strings.Contains(provider.AuthnURL, "authn-k8s") {
log.Printf("Info: Conjur provider using Kubernetes authenticator-based authentication")
// Load the authenticator with the config from the environment, and log in to Conjur
if authenticator, err = loadAuthenticator(provider.AuthnURL, provider.Version, provider.Config); err != nil {
return nil, fmt.Errorf("ERROR: Conjur provider could not retrieve access token using the authenticator client: %s", err)
}
provider.Authenticator = authenticator
refreshErr := provider.fetchAccessToken()
if refreshErr != nil {
return nil, refreshErr
}
go func() {
// Sleep until token needs refresh
time.Sleep(provider.Authenticator.Config.TokenRefreshTimeout)
// Authenticate in a loop
err := provider.fetchAccessTokenLoop()
// On repeated errors in getting the token, we need to exit the
// broker since the provider cannot be used.
if err != nil {
log.Fatal(err)
}
}()
// Once the token file has been loaded, create a new instance of the Conjur client
if conjur, err = conjurapi.NewClientFromTokenFile(provider.Config, authenticatorTokenFile); err != nil {
return nil, fmt.Errorf("ERROR: Could not create new Conjur provider: %s", err)
}
} else {
return nil, errors.New("ERROR: Unable to construct a Conjur provider client from the available credentials")
}
provider.Conjur = conjur
return provider, nil
}
// GetName returns the name of the provider
func (p *Provider) GetName() string {
return p.Name
}
// GetValue obtains a value by ID. The recognized IDs are:
// * "accessToken"
// * Any Conjur variable ID
func (p *Provider) GetValue(id string) ([]byte, error) {
var err error
if id == "accessToken" {
if p.Username != "" && p.APIKey != "" {
// TODO: Use a cached access token from the client, once it's exposed
return p.Conjur.Authenticate(authn.LoginPair{
p.Username,
p.APIKey,
})
}
return nil, errors.New("Error: Conjur provider can't provide an accessToken unless username and apiKey credentials are provided")
}
// If using the Conjur Kubernetes authenticator, ensure that the
// Conjur API is using the current access token
if p.AuthnURL != "" {
if p.Conjur, err = conjurapi.NewClientFromTokenFile(p.Config, authenticatorTokenFile); err != nil {
log.Fatalf("ERROR: Could not create new Conjur provider: %s", err)
}
}
tokens := strings.SplitN(id, ":", 3)
switch len(tokens) {
case 1:
tokens = []string{p.Config.Account, "variable", tokens[0]}
case 2:
tokens = []string{p.Config.Account, tokens[0], tokens[1]}
}
return p.Conjur.RetrieveSecret(strings.Join(tokens, ":"))
}
// loadAuthenticator returns a Conjur Kubernetes authenticator client
// that has performed the login process to retrieve the signed certificate
// from Conjur
// The authenticator will be used to retrieve a time-limited access token
// This method requires CONJUR_ACCOUNT, CONJUR_AUTHN_URL, CONJUR_AUTHN_LOGIN, and
// CONJUR_SSL_CERTIFICATE/CONJUR_CERT_FILE env vars to be present
// if CONJUR_VERSION is not present, it defaults to "5"
// Currently the deployment manifest for Secretless must also specify
// MY_POD_NAMESPACE and MY_POD_NAME from the pod metadata, but there is a GH
// issue logged in the authenticator for doing this via the Kubernetes API
func loadAuthenticator(authnURL string, version string,
providerConfig conjurapi.Config) (*authenticator.Authenticator, error) {
var err error
// Check that required environment variables are set
config, err := authnConfig.NewFromEnv()
if err != nil {
return nil, err
}
// Create new Authenticator
authenticator, err := authenticator.New(*config)
if err != nil {
return nil, err
}
return authenticator, nil
}
// fetchAccessToken uses the Conjur Kubernetes authenticator
// to authenticate with Conjur and retrieve a new time-limited
// access token.
// fetchAccessToken carries out retry with exponential backoff
func (p *Provider) fetchAccessToken() error {
// Configure exponential backoff
expBackoff := backoff.NewExponentialBackOff()
expBackoff.InitialInterval = 2 * time.Second
expBackoff.RandomizationFactor = 0.5
expBackoff.Multiplier = 2
expBackoff.MaxInterval = 15 * time.Second
expBackoff.MaxElapsedTime = 2 * time.Minute
// Authenticate with retries on failure with exponential backoff
err := backoff.Retry(func() error {
// Lock the authenticatorMutex
p.AuthenticationMutex.Lock()
defer p.AuthenticationMutex.Unlock()
log.Printf("Info: Conjur provider is authenticating as %s ...", p.Authenticator.Config.Username)
resp, err := p.Authenticator.Authenticate()
if err != nil {
log.Printf("Info: Conjur provider received an error on authenticate: %s", err.Error())
return err
}
err = p.Authenticator.ParseAuthenticationResponse(resp)
if err != nil {
log.Printf("Error: Conjur provider failure parsing response: %s", err.Error())
return err
}
return nil
}, expBackoff)
if err != nil {
return fmt.Errorf("Error: Conjur provider unable to authenticate; backoff exhausted: %s", err.Error())
}
return nil
}
// fetchAccessTokenLoop runs authenticate in an infinite loop
// punctuated by by sleeps of duration TokenRefreshTimeout
func (p *Provider) fetchAccessTokenLoop() error {
if p.Authenticator == nil {
return errors.New("Error: Conjur Kubernetes authenticator must be instantiated before access token may be refreshed")
}
// Fetch the access token in a loop
for {
err := p.fetchAccessToken()
if err != nil {
return err
}
// sleep until token needs refresh
time.Sleep(p.Authenticator.Config.TokenRefreshTimeout)
}
}