forked from gravitational/teleport
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjoin_ec2.go
386 lines (346 loc) · 12.2 KB
/
join_ec2.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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"slices"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/digitorus/pkcs7"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
)
type ec2Client interface {
DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
}
type ec2ClientKey struct{}
func ec2ClientFromContext(ctx context.Context) (ec2Client, bool) {
ec2Client, ok := ctx.Value(ec2ClientKey{}).(ec2Client)
return ec2Client, ok
}
// ec2ClientFromConfig returns a new ec2 client from the given aws config, or
// may load the client from the passed context if one has been set (for tests).
func ec2ClientFromConfig(ctx context.Context, cfg aws.Config) ec2Client {
ec2Client, ok := ec2ClientFromContext(ctx)
if ok {
return ec2Client
}
return ec2.NewFromConfig(cfg)
}
// checkEC2AllowRules checks that the iid matches at least one of the allow
// rules of the given token.
func checkEC2AllowRules(ctx context.Context, iid *imds.InstanceIdentityDocument, provisionToken types.ProvisionToken) error {
allowRules := provisionToken.GetAllowRules()
for _, rule := range allowRules {
// if this rule specifies an AWS account, the IID must match
if len(rule.AWSAccount) > 0 {
if rule.AWSAccount != iid.AccountID {
continue
}
}
// if this rule specifies any AWS regions, the IID must match one of them
if len(rule.AWSRegions) > 0 {
if !slices.Contains(rule.AWSRegions, iid.Region) {
continue
}
}
// iid matches this allow rule. Check if it is running.
return trace.Wrap(checkInstanceRunning(ctx, iid.InstanceID, iid.Region, rule.AWSRole))
}
return trace.AccessDenied("instance %v did not match any allow rules in token %v", iid.InstanceID, provisionToken.GetName())
}
func checkInstanceRunning(ctx context.Context, instanceID, region, IAMRole string) error {
awsClientConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
return trace.Wrap(err)
}
awsClientConfig.Region = region
// assume the configured IAM role if necessary
if IAMRole != "" {
stsClient := sts.NewFromConfig(awsClientConfig)
creds := stscreds.NewAssumeRoleProvider(stsClient, IAMRole)
awsClientConfig.Credentials = aws.NewCredentialsCache(creds)
}
ec2Client := ec2ClientFromConfig(ctx, awsClientConfig)
output, err := ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
InstanceIds: []string{instanceID},
})
if err != nil {
return trace.Wrap(err)
}
if len(output.Reservations) == 0 || len(output.Reservations[0].Instances) == 0 {
return trace.AccessDenied("failed to get instance state")
}
instance := output.Reservations[0].Instances[0]
if instance.InstanceId == nil || *instance.InstanceId != instanceID {
return trace.AccessDenied("failed to get instance state")
}
if instance.State == nil || instance.State.Name != ec2types.InstanceStateNameRunning {
return trace.AccessDenied("instance is not running")
}
return nil
}
// parseAndVerifyIID parses the given Instance Identity Document and checks that
// the signature is valid.
func parseAndVerifyIID(iidBytes []byte) (*imds.InstanceIdentityDocument, error) {
sigPEM := fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", string(iidBytes))
sigBER, _ := pem.Decode([]byte(sigPEM))
if sigBER == nil {
return nil, trace.AccessDenied("unable to decode Instance Identity Document")
}
p7, err := pkcs7.Parse(sigBER.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
var iid imds.InstanceIdentityDocument
if err := json.Unmarshal(p7.Content, &iid); err != nil {
return nil, trace.Wrap(err)
}
rawCert, ok := awsRSA2048CertBytes[iid.Region]
if !ok {
return nil, trace.AccessDenied("unsupported EC2 region: %q", iid.Region)
}
certPEM, _ := pem.Decode(rawCert)
cert, err := x509.ParseCertificate(certPEM.Bytes)
if err != nil {
return nil, trace.Wrap(err)
}
p7.Certificates = []*x509.Certificate{cert}
if err = p7.Verify(); err != nil {
return nil, trace.AccessDenied("invalid signature")
}
return &iid, nil
}
func checkPendingTime(iid *imds.InstanceIdentityDocument, provisionToken types.ProvisionToken, clock clockwork.Clock) error {
timeSinceInstanceStart := clock.Since(iid.PendingTime)
// Sanity check IID is not from the future. Allow 1 minute of clock drift.
if timeSinceInstanceStart < -1*time.Minute {
return trace.AccessDenied("Instance Identity Document PendingTime appears to be in the future")
}
ttl := time.Duration(provisionToken.GetAWSIIDTTL())
if timeSinceInstanceStart > ttl {
return trace.AccessDenied("Instance Identity Document with PendingTime %v is older than configured TTL of %v", iid.PendingTime, ttl)
}
return nil
}
func nodeExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
namespaces, err := presence.GetNamespaces()
if err != nil {
return false, trace.Wrap(err)
}
for _, namespace := range namespaces {
_, err := presence.GetNode(ctx, namespace.GetName(), hostID)
if trace.IsNotFound(err) {
continue
} else if err != nil {
return false, trace.Wrap(err)
} else {
return true, nil
}
}
return false, nil
}
func proxyExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
proxies, err := presence.GetProxies()
if err != nil {
return false, trace.Wrap(err)
}
for _, proxy := range proxies {
if proxy.GetName() == hostID {
return true, nil
}
}
return false, nil
}
func kubeExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
kubes, err := presence.GetKubernetesServers(ctx)
if err != nil {
return false, trace.Wrap(err)
}
for _, kube := range kubes {
if kube.GetHostID() == hostID {
return true, nil
}
}
return false, nil
}
func appExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
namespaces, err := presence.GetNamespaces()
if err != nil {
return false, trace.Wrap(err)
}
for _, namespace := range namespaces {
apps, err := presence.GetApplicationServers(ctx, namespace.GetName())
if err != nil {
return false, trace.Wrap(err)
}
for _, app := range apps {
if app.GetName() == hostID {
return true, nil
}
}
}
return false, nil
}
func dbExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
namespaces, err := presence.GetNamespaces()
if err != nil {
return false, trace.Wrap(err)
}
for _, namespace := range namespaces {
dbs, err := presence.GetDatabaseServers(ctx, namespace.GetName())
if err != nil {
return false, trace.Wrap(err)
}
for _, db := range dbs {
if db.GetName() == hostID {
return true, nil
}
}
}
return false, nil
}
func oktaExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
namespaces, err := presence.GetNamespaces()
if err != nil {
return false, trace.Wrap(err)
}
for _, namespace := range namespaces {
apps, err := presence.GetApplicationServers(ctx, namespace.GetName())
if err != nil {
return false, trace.Wrap(err)
}
for _, app := range apps {
if app.GetName() == hostID && app.Origin() == types.OriginOkta {
return true, nil
}
}
}
return false, nil
}
func desktopServiceExists(ctx context.Context, presence services.Presence, hostID string) (bool, error) {
svcs, err := presence.GetWindowsDesktopServices(ctx)
if err != nil {
return false, trace.Wrap(err)
}
for _, wds := range svcs {
if wds.GetName() == hostID {
return true, nil
}
}
return false, nil
}
// tryToDetectIdentityReuse performs a best-effort check to see if the specified role+id combination
// is already in use by an instance. This will only detect re-use in the case where a recent heartbeat
// clearly shows the combination in use since teleport maintains no long-term per-instance state.
func (a *Server) tryToDetectIdentityReuse(ctx context.Context, req *types.RegisterUsingTokenRequest, iid *imds.InstanceIdentityDocument) error {
requestedHostID := req.HostID
expectedHostID := utils.NodeIDFromIID(iid)
if requestedHostID != expectedHostID {
return trace.AccessDenied("invalid host ID %q, expected %q", requestedHostID, expectedHostID)
}
var instanceExists bool
var err error
switch req.Role {
case types.RoleNode:
instanceExists, err = nodeExists(ctx, a, req.HostID)
case types.RoleProxy:
instanceExists, err = proxyExists(ctx, a, req.HostID)
case types.RoleKube:
instanceExists, err = kubeExists(ctx, a, req.HostID)
case types.RoleApp:
instanceExists, err = appExists(ctx, a, req.HostID)
case types.RoleDatabase:
instanceExists, err = dbExists(ctx, a, req.HostID)
case types.RoleWindowsDesktop:
instanceExists, err = desktopServiceExists(ctx, a, req.HostID)
case types.RoleOkta:
instanceExists, err = oktaExists(ctx, a, req.HostID)
case types.RoleInstance:
// no appropriate check exists for the Instance role
instanceExists = false
case types.RoleDiscovery:
// no appropriate check exists for the Discovery role
instanceExists = false
case types.RoleMDM:
// no appropriate check exists for the MDM role
instanceExists = false
default:
return trace.BadParameter("unsupported role: %q", req.Role)
}
if err != nil {
return trace.Wrap(err)
}
if instanceExists {
log.Warnf("Server with ID %q and role %q is attempting to join the cluster with a Simplified Node Joining request, but"+
" a server with this ID is already present in the cluster.", req.HostID, req.Role)
return trace.AccessDenied("server with host ID %q and role %q already exists", req.HostID, req.Role)
}
return nil
}
// checkEC2JoinRequest checks register requests which use EC2 Simplified Node
// Joining. This method checks that:
// 1. The given Instance Identity Document has a valid signature (signed by AWS).
// 2. There is no obvious signs that a node already joined the cluster from this EC2 instance (to
// reduce the risk of re-use of a stolen Instance Identity Document).
// 3. The signed instance attributes match one of the allow rules for the
// corresponding token.
//
// If the request does not include an Instance Identity Document, and the
// token does not include any allow rules, this method returns nil and the
// normal token checking logic resumes.
func (a *Server) checkEC2JoinRequest(ctx context.Context, req *types.RegisterUsingTokenRequest) error {
tokenName := req.Token
provisionToken, err := a.GetToken(ctx, tokenName)
if err != nil {
return trace.Wrap(err)
}
log.Debugf("Received Simplified Node Joining request for host %q", req.HostID)
if len(req.EC2IdentityDocument) == 0 {
return trace.AccessDenied("this token is only valid for the EC2 join " +
"method but the node has not included an EC2 Instance Identity " +
"Document, make sure your node is configured to use the EC2 join method")
}
iid, err := parseAndVerifyIID(req.EC2IdentityDocument)
if err != nil {
return trace.Wrap(err)
}
if err := checkPendingTime(iid, provisionToken, a.clock); err != nil {
return trace.Wrap(err)
}
if err := a.tryToDetectIdentityReuse(ctx, req, iid); err != nil {
return trace.Wrap(err)
}
if err := checkEC2AllowRules(ctx, iid, provisionToken); err != nil {
return trace.Wrap(err)
}
return nil
}