forked from meltwater/secretary
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdaemon.go
258 lines (218 loc) · 9.12 KB
/
daemon.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
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strings"
)
const unixTimeRFC3339Str string = "1970-01-01T00:00:00.000Z"
// DaemonRequest is the request expected by the /v1/decrypt endpoint
// RequestedSecret is the Secret encrypted with master key
// Key is an optional Name (Enva Var key) of RequestedSecret
type DaemonRequest struct {
AppID, AppVersion, TaskID string
RequestedSecret string
Key string
}
// DaemonResponse is the response returned by the /v1/decrypt endpoint
type DaemonResponse struct {
PlaintextSecret string
}
// DaemonStatusResponse is the response returned by the /v1/status endpoint
type DaemonStatusResponse struct {
Status string
}
// AppOrTask holds the result struct for Apps and Task returned by getMesosTask or getMarathonApp
type AppOrTask struct {
Name, ID, Version, TaskID, State string
DeployKey *[32]byte
ServiceKey *[32]byte
Env map[string]string
Labels map[string]string
}
func errorResponse(w http.ResponseWriter, r *http.Request, err interface{}, statusCode int) {
log.Printf("HTTP %d from %s: %s", statusCode, r.RemoteAddr, err)
http.Error(w, fmt.Sprintf("%s", err), statusCode)
}
func decryptRequest(at *AppOrTask, masterKey *[32]byte, serviceEnvelope string) (*DaemonRequest, error) {
// Authenticate with deploy key and decrypt
body, err := decryptEnvelope(at.DeployKey, masterKey, serviceEnvelope)
if err != nil {
return nil, fmt.Errorf("Failed to authenticate/decrypt request using deploy and master key (incorrect master key or hacking attempt? (%s))", err)
}
// Authenticate with optional service key and decrypt
if at.ServiceKey != nil {
body, err = decryptEnvelope(at.ServiceKey, masterKey, string(body))
if err != nil {
return nil, fmt.Errorf("Failed to authenticate/decrypt request using service and master key (incorrect master key or hacking attempt? (%s))", err)
}
}
// Unpack request struct
var request DaemonRequest
err = json.Unmarshal(body, &request)
if err != nil {
return nil, fmt.Errorf("Failed to parse JSON request (%s)", err)
}
if at.Version != "" && at.ID != "" {
// Validate that appId, appVersion, taskId corresponds to HTTP request params
// Parse the timestamps identifying app versions as Time to prevent issues with missing "0" when comparing as str
requestAppVersion, _ := strToTimeRFC3339(request.AppVersion)
marathonAppVersion, _ := strToTimeRFC3339(at.Version)
if request.AppID != at.ID || !requestAppVersion.Equal(marathonAppVersion) || request.TaskID != at.TaskID {
return nil, errors.New("Given appid, appversion or taskid doesn't correspond to HTTP request params (bug or hacking attempt?)")
}
} else if request.TaskID != at.TaskID {
return nil, errors.New("Given taskid doesn't correspond to HTTP request params (bug or hacking attempt?)")
}
return &request, nil
}
func verifyAuthorization(at *AppOrTask, request *DaemonRequest) (bool, error) {
// Verify that encrypted string is present in app config / mesos task definition
if at.ID != "" && at.Version != "" && len(at.Env) > 0 {
// If it's a marathon app, we use the old method and check the value belongs to then env in the target
for _, value := range at.Env {
if strings.Contains(stripWhitespace(value), request.RequestedSecret) {
return true, nil
}
}
return false, errors.New("Given secret isn't part of app config (bug or hacking attempt?)")
} else if len(at.Labels) > 0 && request.Key != "" {
// if it is a mesos task, we require the env var is passed in the envelope and ALLOWED SECRETS
// are allowed in the label (we don't have access to the env)
allowedSecretsVars, ok := at.Labels["ALLOWED_SECRETS_VARS"]
if !ok {
return false, errors.New("Given mesos task secret requires the label ALLOWED_SECRETS_VARS set (bug or hacking attempt?)")
}
allowedVars := strings.Split(allowedSecretsVars, ",")
for _, envVarKey := range allowedVars {
if stripWhitespace(envVarKey) == request.Key {
return true, nil
}
}
return false, errors.New("Given secret is not in an env var allowed in ALLOWED_SECRETS_VARS (bug or hacking attempt?)")
}
return false, errors.New("We cannot verify the requested secret (bug or hacking attempt?)")
}
func encryptResponse(at *AppOrTask, masterKey *[32]byte, plaintext []byte) ([]byte, error) {
message := DaemonResponse{PlaintextSecret: encode(plaintext)}
encoded, err := json.Marshal(message)
if err != nil {
return nil, err
}
// Encrypt with service key
response := string(encoded)
if at.ServiceKey != nil {
response, err = encryptEnvelope(at.ServiceKey, masterKey, []byte(response))
if err != nil {
return nil, err
}
}
// Encrypt with deploy key
encrypted, err := encryptEnvelope(at.DeployKey, masterKey, []byte(response))
if err != nil {
return nil, err
}
return []byte(encrypted), nil
}
func decryptEndpointHandler(marathonURL, mesosLeaderURL string, masterKey *[32]byte, strategy DecryptionStrategy) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
errorResponse(w, r, "Expected POST method", http.StatusMethodNotAllowed)
return
}
err := r.ParseForm()
if err != nil {
errorResponse(w, r, "Expected application/x-www-form-urlencoded request body", http.StatusUnsupportedMediaType)
return
}
appID := r.Form.Get("appid")
appVersion := r.Form.Get("appversion")
taskID := r.Form.Get("taskid")
serviceEnvelope := r.Form.Get("envelope")
log.Printf("Received request from %s (%s, %s) at %s with envelope %s", appID, taskID, appVersion, r.RemoteAddr, ellipsis(serviceEnvelope, 64))
var at *AppOrTask
// Jobs use marathon under the hood, so the client will collect the same MARATHON_ variables
// We will need to distinguish by checking whether appVersion has a time string content
// and if it does, whether it is unix time
itIsAppVersion := false
if appVersion != "" {
unixTimeRFC3339, _ := strToTimeRFC3339(unixTimeRFC3339Str)
appVersionTimeRFC3339, errav := strToTimeRFC3339(appVersion)
if errav != nil {
log.Printf("App Version for taskID %s came in the wrong format, unable to parse it as time RFC3339. It was: %s", taskID, errav)
errorResponse(w, r, "Expected appVersion in valid RFC 3339. Wrong format given", http.StatusUnprocessableEntity)
return
} else if !unixTimeRFC3339.Equal(appVersionTimeRFC3339) {
itIsAppVersion = true
} else {
log.Printf("App Version for taskID %s is a unix timestamp", taskID)
}
}
if appID != "" && taskID != "" && itIsAppVersion && serviceEnvelope != "" {
log.Printf("Using marathon at %s for appID %s", marathonURL, appID)
at, err = getMarathonApp(marathonURL, appID, appVersion, taskID)
if err != nil {
errorResponse(w, r, err, http.StatusInternalServerError)
return
}
} else if taskID != "" && serviceEnvelope != "" && !itIsAppVersion {
log.Printf("Using mesos at %s for task with ID %s", mesosLeaderURL, taskID)
at, err = getMesosTasks(mesosLeaderURL, taskID)
if err != nil {
errorResponse(w, r, err, http.StatusInternalServerError)
return
}
} else {
errorResponse(w, r, errors.New("Expected parameters, one of {appid, appversion, taskid, envelope} missing"), http.StatusBadRequest)
return
}
// Authenticate and decrypt request
request, err := decryptRequest(at, masterKey, serviceEnvelope)
if err != nil {
errorResponse(w, r, err, http.StatusBadRequest)
return
}
// Verify that the secret is actually part of the config or authorized by it
ok, err := verifyAuthorization(at, request)
if !ok || err != nil {
errorResponse(w, r, err, http.StatusUnauthorized)
return
}
// Authenticate with config key and decrypt secret
plaintext, err := strategy.Decrypt(request.RequestedSecret, request.Key)
if err != nil {
errorResponse(w, r, fmt.Errorf("Failed to decrypt plaintext secret, incorrect config or master key? (%s)", err), http.StatusBadRequest)
return
}
encrypted, err := encryptResponse(at, masterKey, plaintext)
if err != nil {
errorResponse(w, r, err, http.StatusInternalServerError)
return
}
w.Write([]byte(encrypted))
}
}
func statusEndpointHandler() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
message := DaemonStatusResponse{Status: "OK"}
encoded, err := json.Marshal(message)
if err != nil {
errorResponse(w, r, fmt.Errorf("Failed to serialize json response (%s)", err), http.StatusInternalServerError)
return
}
w.Write(encoded)
}
}
func daemonCommand(listenAddress, marathonURL, mesosLeaderURL string, masterKey *[32]byte, tlsCertFile string, tlsKeyFile string, strategy DecryptionStrategy) {
http.HandleFunc("/v1/decrypt", decryptEndpointHandler(marathonURL, mesosLeaderURL, masterKey, strategy))
http.HandleFunc("/v1/status", statusEndpointHandler())
if tlsCertFile != "" && tlsKeyFile != "" {
log.Printf("Daemon listening on TLS %s", listenAddress)
log.Fatal(http.ListenAndServeTLS(listenAddress, tlsCertFile, tlsKeyFile, nil))
} else {
log.Printf("Daemon listening on %s", listenAddress)
log.Fatal(http.ListenAndServe(listenAddress, nil))
}
}