Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
小 白蛋
Vault
Commits
b548e286
Commit
b548e286
authored
8 years ago
by
louism517
Committed by
Jeff Mitchell
8 years ago
Browse files
Options
Download
Email Patches
Plain Diff
Support for Cross-Account AWS Auth (#2148)
parent
af105b91
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
builtin/credential/aws-ec2/backend.go
+15
-13
builtin/credential/aws-ec2/backend.go
builtin/credential/aws-ec2/backend_test.go
+114
-0
builtin/credential/aws-ec2/backend_test.go
builtin/credential/aws-ec2/client.go
+82
-17
builtin/credential/aws-ec2/client.go
builtin/credential/aws-ec2/path_config_sts.go
+248
-0
builtin/credential/aws-ec2/path_config_sts.go
builtin/credential/aws-ec2/path_login.go
+39
-7
builtin/credential/aws-ec2/path_login.go
website/source/docs/auth/aws-ec2.html.md
+158
-0
website/source/docs/auth/aws-ec2.html.md
with
656 additions
and
37 deletions
+656
-37
builtin/credential/aws-ec2/backend.go
+
15
-
13
View file @
b548e286
...
...
@@ -45,17 +45,17 @@ type backend struct {
// of tidyCooldownPeriod.
nextTidyTime
time
.
Time
// Map to hold the EC2 client objects indexed by region
. This avoids the
// overhead of creating a client object for every login request.
When
// the credentials are modified or deleted, all the cached client objects
// will be flushed.
EC2ClientsMap
map
[
string
]
*
ec2
.
EC2
// Map to hold the IAM client objects indexed by region
. This avoids
// the overhead of creating a client object for every login request.
// When the credentials are modified or deleted, all the cached client
//
objects
will be flushed.
IAMClientsMap
map
[
string
]
*
iam
.
IAM
// Map to hold the EC2 client objects indexed by region
and STS role.
//
This avoids the
overhead of creating a client object for every login request.
//
When
the credentials are modified or deleted, all the cached client objects
// will be flushed.
The empty STS role signifies the master account
EC2ClientsMap
map
[
string
]
map
[
string
]
*
ec2
.
EC2
// Map to hold the IAM client objects indexed by region
and STS role.
//
This avoids
the overhead of creating a client object for every login request.
// When the credentials are modified or deleted, all the cached client
objects
// will be flushed.
The empty STS role signifies the master account
IAMClientsMap
map
[
string
]
map
[
string
]
*
iam
.
IAM
}
func
Backend
(
conf
*
logical
.
BackendConfig
)
(
*
backend
,
error
)
{
...
...
@@ -71,8 +71,8 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
// If there is a real need, this can be made configurable.
tidyCooldownPeriod
:
time
.
Hour
,
Salt
:
salt
,
EC2ClientsMap
:
make
(
map
[
string
]
*
ec2
.
EC2
),
IAMClientsMap
:
make
(
map
[
string
]
*
iam
.
IAM
),
EC2ClientsMap
:
make
(
map
[
string
]
map
[
string
]
*
ec2
.
EC2
),
IAMClientsMap
:
make
(
map
[
string
]
map
[
string
]
*
iam
.
IAM
),
}
b
.
Backend
=
&
framework
.
Backend
{
...
...
@@ -92,6 +92,8 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
pathRoleTag
(
b
),
pathConfigClient
(
b
),
pathConfigCertificate
(
b
),
pathConfigSts
(
b
),
pathListSts
(
b
),
pathConfigTidyRoletagBlacklist
(
b
),
pathConfigTidyIdentityWhitelist
(
b
),
pathListCertificates
(
b
),
...
...
This diff is collapsed.
Click to expand it.
builtin/credential/aws-ec2/backend_test.go
+
114
-
0
View file @
b548e286
...
...
@@ -1275,3 +1275,117 @@ func TestBackendAcc_LoginAndWhitelistIdentity(t *testing.T) {
t
.
Fatalf
(
"login attempt failed"
)
}
}
func
TestBackend_pathStsConfig
(
t
*
testing
.
T
)
{
config
:=
logical
.
TestBackendConfig
()
storage
:=
&
logical
.
InmemStorage
{}
config
.
StorageView
=
storage
b
,
err
:=
Backend
(
config
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
_
,
err
=
b
.
Setup
(
config
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
stsReq
:=
&
logical
.
Request
{
Operation
:
logical
.
CreateOperation
,
Storage
:
storage
,
Path
:
"config/sts/account1"
,
}
checkFound
,
exists
,
err
:=
b
.
HandleExistenceCheck
(
stsReq
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
!
checkFound
{
t
.
Fatal
(
"existence check not found for path 'config/sts/account1'"
)
}
if
exists
{
t
.
Fatal
(
"existence check should have returned 'false' for 'config/sts/account1'"
)
}
data
:=
map
[
string
]
interface
{}{
"sts_role"
:
"arn:aws:iam:account1:role/myRole"
,
}
stsReq
.
Data
=
data
// test create operation
resp
,
err
:=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
||
(
resp
!=
nil
&&
resp
.
IsError
())
{
t
.
Fatalf
(
"resp: %#v, err: %v"
,
resp
,
err
)
}
stsReq
.
Data
=
nil
// test existence check
checkFound
,
exists
,
err
=
b
.
HandleExistenceCheck
(
stsReq
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
!
checkFound
{
t
.
Fatal
(
"existence check not found for path 'config/sts/account1'"
)
}
if
!
exists
{
t
.
Fatal
(
"existence check should have returned 'true' for 'config/sts/account1'"
)
}
stsReq
.
Operation
=
logical
.
ReadOperation
// test read operation
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
expectedStsRole
:=
"arn:aws:iam:account1:role/myRole"
if
resp
.
Data
[
"sts_role"
]
.
(
string
)
!=
expectedStsRole
{
t
.
Fatalf
(
"bad: expected:%s
\n
got:%s
\n
"
,
expectedStsRole
,
resp
.
Data
[
"sts_role"
]
.
(
string
))
}
stsReq
.
Operation
=
logical
.
CreateOperation
stsReq
.
Path
=
"config/sts/account2"
stsReq
.
Data
=
data
// create another entry to test the list operation
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
||
(
resp
!=
nil
&&
resp
.
IsError
())
{
t
.
Fatal
(
err
)
}
stsReq
.
Operation
=
logical
.
ListOperation
stsReq
.
Path
=
"config/sts"
// test list operation
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
resp
==
nil
||
resp
.
IsError
()
{
t
.
Fatalf
(
"failed to list config/sts"
)
}
keys
:=
resp
.
Data
[
"keys"
]
.
([]
string
)
if
len
(
keys
)
!=
2
{
t
.
Fatalf
(
"invalid keys listed: %#v
\n
"
,
keys
)
}
stsReq
.
Operation
=
logical
.
DeleteOperation
stsReq
.
Path
=
"config/sts/account1"
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
||
(
resp
!=
nil
&&
resp
.
IsError
())
{
t
.
Fatal
(
err
)
}
stsReq
.
Path
=
"config/sts/account2"
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
||
(
resp
!=
nil
&&
resp
.
IsError
())
{
t
.
Fatal
(
err
)
}
stsReq
.
Operation
=
logical
.
ListOperation
stsReq
.
Path
=
"config/sts"
// test list operation
resp
,
err
=
b
.
HandleRequest
(
stsReq
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
resp
==
nil
||
resp
.
IsError
()
{
t
.
Fatalf
(
"failed to list config/sts"
)
}
if
resp
.
Data
[
"keys"
]
!=
nil
{
t
.
Fatalf
(
"no entries should be present"
)
}
}
This diff is collapsed.
Click to expand it.
builtin/credential/aws-ec2/client.go
+
82
-
17
View file @
b548e286
...
...
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
...
...
@@ -48,7 +49,7 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config
return
nil
,
err
}
if
creds
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not compile valid credential providers from static config, environ
e
mnt, shared, or instance metadata"
)
return
nil
,
fmt
.
Errorf
(
"could not compile valid credential providers from static config, environm
e
nt, shared, or instance metadata"
)
}
// Create a config that can be used to make the API calls.
...
...
@@ -60,6 +61,29 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config
},
nil
}
// getStsClientConfig returns an aws-sdk-go config, with assumed credentials
// It uses getClientConfig to obtain config for the runtime environemnt, which is
// then used to obtain a set of assumed credentials. The credentials will expire
// after 15 minutes but will auto-refresh.
func
(
b
*
backend
)
getStsClientConfig
(
s
logical
.
Storage
,
region
string
,
stsRole
string
)
(
*
aws
.
Config
,
error
)
{
config
,
err
:=
b
.
getClientConfig
(
s
,
region
)
if
err
!=
nil
{
return
nil
,
err
}
if
config
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not compile valid credentials through the default provider chain"
)
}
assumedCredentials
:=
stscreds
.
NewCredentials
(
session
.
New
(
config
),
stsRole
)
// Test that we actually have permissions to assume the role
if
_
,
err
=
assumedCredentials
.
Get
();
err
!=
nil
{
return
nil
,
err
}
config
.
Credentials
=
assumedCredentials
return
config
,
nil
}
// flushCachedEC2Clients deletes all the cached ec2 client objects from the backend.
// If the client credentials configuration is deleted or updated in the backend, all
// the cached EC2 client objects will be flushed. Config mutex lock should be
...
...
@@ -83,12 +107,12 @@ func (b *backend) flushCachedIAMClients() {
}
// clientEC2 creates a client to interact with AWS EC2 API
func
(
b
*
backend
)
clientEC2
(
s
logical
.
Storage
,
region
string
)
(
*
ec2
.
EC2
,
error
)
{
func
(
b
*
backend
)
clientEC2
(
s
logical
.
Storage
,
region
string
,
stsRole
string
)
(
*
ec2
.
EC2
,
error
)
{
b
.
configMutex
.
RLock
()
if
b
.
EC2ClientsMap
[
region
]
!=
nil
{
if
b
.
EC2ClientsMap
[
region
]
!=
nil
&&
b
.
EC2ClientsMap
[
region
][
stsRole
]
!=
nil
{
defer
b
.
configMutex
.
RUnlock
()
// If the client object was already created, return it
return
b
.
EC2ClientsMap
[
region
],
nil
return
b
.
EC2ClientsMap
[
region
]
[
stsRole
]
,
nil
}
// Release the read lock and acquire the write lock
...
...
@@ -97,28 +121,49 @@ func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error)
defer
b
.
configMutex
.
Unlock
()
// If the client gets created while switching the locks, return it
if
b
.
EC2ClientsMap
[
region
]
!=
nil
{
return
b
.
EC2ClientsMap
[
region
],
nil
if
b
.
EC2ClientsMap
[
region
]
!=
nil
&&
b
.
EC2ClientsMap
[
region
][
stsRole
]
!=
nil
{
return
b
.
EC2ClientsMap
[
region
]
[
stsRole
]
,
nil
}
// Create an AWS config object using a chain of providers
awsConfig
,
err
:=
b
.
getClientConfig
(
s
,
region
)
var
awsConfig
*
aws
.
Config
var
err
error
// The empty stsRole signifies the master account
if
stsRole
==
""
{
awsConfig
,
err
=
b
.
getClientConfig
(
s
,
region
)
}
else
{
awsConfig
,
err
=
b
.
getStsClientConfig
(
s
,
region
,
stsRole
)
}
if
err
!=
nil
{
return
nil
,
err
}
if
awsConfig
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not retrieve valid assumed credentials"
)
}
// Create a new EC2 client object, cache it and return the same
b
.
EC2ClientsMap
[
region
]
=
ec2
.
New
(
session
.
New
(
awsConfig
))
return
b
.
EC2ClientsMap
[
region
],
nil
client
:=
ec2
.
New
(
session
.
New
(
awsConfig
))
if
client
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not obtain ec2 client"
)
}
if
_
,
ok
:=
b
.
EC2ClientsMap
[
region
];
!
ok
{
b
.
EC2ClientsMap
[
region
]
=
map
[
string
]
*
ec2
.
EC2
{
stsRole
:
client
}
}
else
{
b
.
EC2ClientsMap
[
region
][
stsRole
]
=
client
}
return
b
.
EC2ClientsMap
[
region
][
stsRole
],
nil
}
// clientIAM creates a client to interact with AWS IAM API
func
(
b
*
backend
)
clientIAM
(
s
logical
.
Storage
,
region
string
)
(
*
iam
.
IAM
,
error
)
{
func
(
b
*
backend
)
clientIAM
(
s
logical
.
Storage
,
region
string
,
stsRole
string
)
(
*
iam
.
IAM
,
error
)
{
b
.
configMutex
.
RLock
()
if
b
.
IAMClientsMap
[
region
]
!=
nil
{
if
b
.
IAMClientsMap
[
region
]
!=
nil
&&
b
.
IAMClientsMap
[
region
][
stsRole
]
!=
nil
{
defer
b
.
configMutex
.
RUnlock
()
// If the client object was already created, return it
return
b
.
IAMClientsMap
[
region
],
nil
return
b
.
IAMClientsMap
[
region
]
[
stsRole
]
,
nil
}
// Release the read lock and acquire the write lock
...
...
@@ -127,17 +172,37 @@ func (b *backend) clientIAM(s logical.Storage, region string) (*iam.IAM, error)
defer
b
.
configMutex
.
Unlock
()
// If the client gets created while switching the locks, return it
if
b
.
IAMClientsMap
[
region
]
!=
nil
{
return
b
.
IAMClientsMap
[
region
],
nil
if
b
.
IAMClientsMap
[
region
]
!=
nil
&&
b
.
IAMClientsMap
[
region
][
stsRole
]
!=
nil
{
return
b
.
IAMClientsMap
[
region
]
[
stsRole
]
,
nil
}
// Create an AWS config object using a chain of providers
awsConfig
,
err
:=
b
.
getClientConfig
(
s
,
region
)
var
awsConfig
*
aws
.
Config
var
err
error
// The empty stsRole signifies the master account
if
stsRole
==
""
{
awsConfig
,
err
=
b
.
getClientConfig
(
s
,
region
)
}
else
{
awsConfig
,
err
=
b
.
getStsClientConfig
(
s
,
region
,
stsRole
)
}
if
err
!=
nil
{
return
nil
,
err
}
if
awsConfig
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not retrieve valid assumed credentials"
)
}
// Create a new IAM client object, cache it and return the same
b
.
IAMClientsMap
[
region
]
=
iam
.
New
(
session
.
New
(
awsConfig
))
return
b
.
IAMClientsMap
[
region
],
nil
client
:=
iam
.
New
(
session
.
New
(
awsConfig
))
if
client
==
nil
{
return
nil
,
fmt
.
Errorf
(
"could not obtain iam client"
)
}
if
_
,
ok
:=
b
.
IAMClientsMap
[
region
];
!
ok
{
b
.
IAMClientsMap
[
region
]
=
map
[
string
]
*
iam
.
IAM
{
stsRole
:
client
}
}
else
{
b
.
IAMClientsMap
[
region
][
stsRole
]
=
client
}
return
b
.
IAMClientsMap
[
region
][
stsRole
],
nil
}
This diff is collapsed.
Click to expand it.
builtin/credential/aws-ec2/path_config_sts.go
0 → 100644
+
248
-
0
View file @
b548e286
package
awsec2
import
(
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// awsStsEntry is used to store details of an STS role for assumption
type
awsStsEntry
struct
{
StsRole
string
`json:"sts_role" structs:"sts_role" mapstructure:"sts_role"`
}
func
pathListSts
(
b
*
backend
)
*
framework
.
Path
{
return
&
framework
.
Path
{
Pattern
:
"config/sts/?"
,
Callbacks
:
map
[
logical
.
Operation
]
framework
.
OperationFunc
{
logical
.
ListOperation
:
b
.
pathStsList
,
},
HelpSynopsis
:
pathListStsHelpSyn
,
HelpDescription
:
pathListStsHelpDesc
,
}
}
func
pathConfigSts
(
b
*
backend
)
*
framework
.
Path
{
return
&
framework
.
Path
{
Pattern
:
"config/sts/"
+
framework
.
GenericNameRegex
(
"account_id"
),
Fields
:
map
[
string
]
*
framework
.
FieldSchema
{
"account_id"
:
{
Type
:
framework
.
TypeString
,
Description
:
`AWS account ID to be associated with STS role. If set,
Vault will use assumed credentials to verify any login attempts from EC2
instances in this account.`
,
},
"sts_role"
:
{
Type
:
framework
.
TypeString
,
Description
:
`AWS ARN for STS role to be assumed when interacting with the account specified.
The Vault server must have permissions to assume this role.`
,
},
},
ExistenceCheck
:
b
.
pathConfigStsExistenceCheck
,
Callbacks
:
map
[
logical
.
Operation
]
framework
.
OperationFunc
{
logical
.
CreateOperation
:
b
.
pathConfigStsCreateUpdate
,
logical
.
UpdateOperation
:
b
.
pathConfigStsCreateUpdate
,
logical
.
ReadOperation
:
b
.
pathConfigStsRead
,
logical
.
DeleteOperation
:
b
.
pathConfigStsDelete
,
},
HelpSynopsis
:
pathConfigStsSyn
,
HelpDescription
:
pathConfigStsDesc
,
}
}
// Establishes dichotomy of request operation between CreateOperation and UpdateOperation.
// Returning 'true' forces an UpdateOperation, CreateOperation otherwise.
func
(
b
*
backend
)
pathConfigStsExistenceCheck
(
req
*
logical
.
Request
,
data
*
framework
.
FieldData
)
(
bool
,
error
)
{
accountID
:=
data
.
Get
(
"account_id"
)
.
(
string
)
if
accountID
==
""
{
return
false
,
fmt
.
Errorf
(
"missing account_id"
)
}
entry
,
err
:=
b
.
lockedAwsStsEntry
(
req
.
Storage
,
accountID
)
if
err
!=
nil
{
return
false
,
err
}
return
entry
!=
nil
,
nil
}
// pathStsList is used to list all the AWS STS role configurations
func
(
b
*
backend
)
pathStsList
(
req
*
logical
.
Request
,
data
*
framework
.
FieldData
)
(
*
logical
.
Response
,
error
)
{
b
.
configMutex
.
RLock
()
defer
b
.
configMutex
.
RUnlock
()
sts
,
err
:=
req
.
Storage
.
List
(
"config/sts/"
)
if
err
!=
nil
{
return
nil
,
err
}
return
logical
.
ListResponse
(
sts
),
nil
}
// nonLockedSetAwsStsEntry creates or updates an STS role association with the given accountID
// This method does not acquire the write lock before creating or updating. If locking is
// desired, use lockedSetAwsStsEntry instead
func
(
b
*
backend
)
nonLockedSetAwsStsEntry
(
s
logical
.
Storage
,
accountID
string
,
stsEntry
*
awsStsEntry
)
error
{
if
accountID
==
""
{
return
fmt
.
Errorf
(
"missing AWS account ID"
)
}
if
stsEntry
==
nil
{
return
fmt
.
Errorf
(
"missing AWS STS Role ARN"
)
}
entry
,
err
:=
logical
.
StorageEntryJSON
(
"config/sts/"
+
accountID
,
stsEntry
)
if
err
!=
nil
{
return
err
}
if
entry
==
nil
{
return
fmt
.
Errorf
(
"failed to create storage entry for AWS STS configuration"
)
}
return
s
.
Put
(
entry
)
}
// lockedSetAwsStsEntry creates or updates an STS role association with the given accountID
// This method acquires the write lock before creating or updating the STS entry.
func
(
b
*
backend
)
lockedSetAwsStsEntry
(
s
logical
.
Storage
,
accountID
string
,
stsEntry
*
awsStsEntry
)
error
{
if
accountID
==
""
{
return
fmt
.
Errorf
(
"missing AWS account ID"
)
}
if
stsEntry
==
nil
{
return
fmt
.
Errorf
(
"missing sts entry"
)
}
b
.
configMutex
.
Lock
()
defer
b
.
configMutex
.
Unlock
()
return
b
.
nonLockedSetAwsStsEntry
(
s
,
accountID
,
stsEntry
)
}
// nonLockedAwsStsEntry returns the STS role associated with the given accountID.
// This method does not acquire the read lock before returning information. If locking is
// desired, use lockedAwsStsEntry instead
func
(
b
*
backend
)
nonLockedAwsStsEntry
(
s
logical
.
Storage
,
accountID
string
)
(
*
awsStsEntry
,
error
)
{
entry
,
err
:=
s
.
Get
(
"config/sts/"
+
accountID
)
if
err
!=
nil
{
return
nil
,
err
}
if
entry
==
nil
{
return
nil
,
nil
}
var
stsEntry
awsStsEntry
if
err
:=
entry
.
DecodeJSON
(
&
stsEntry
);
err
!=
nil
{
return
nil
,
err
}
return
&
stsEntry
,
nil
}
// lockedAwsStsEntry returns the STS role associated with the given accountID.
// This method acquires the read lock before returning the association.
func
(
b
*
backend
)
lockedAwsStsEntry
(
s
logical
.
Storage
,
accountID
string
)
(
*
awsStsEntry
,
error
)
{
b
.
configMutex
.
RLock
()
defer
b
.
configMutex
.
RUnlock
()
return
b
.
nonLockedAwsStsEntry
(
s
,
accountID
)
}
// pathConfigStsRead is used to return information about an STS role/AWS accountID association
func
(
b
*
backend
)
pathConfigStsRead
(
req
*
logical
.
Request
,
data
*
framework
.
FieldData
)
(
*
logical
.
Response
,
error
)
{
accountID
:=
data
.
Get
(
"account_id"
)
.
(
string
)
if
accountID
==
""
{
return
logical
.
ErrorResponse
(
"missing account id"
),
nil
}
stsEntry
,
err
:=
b
.
lockedAwsStsEntry
(
req
.
Storage
,
accountID
)
if
err
!=
nil
{
return
nil
,
err
}
if
stsEntry
==
nil
{
return
nil
,
nil
}
return
&
logical
.
Response
{
Data
:
structs
.
New
(
stsEntry
)
.
Map
(),
},
nil
}
// pathConfigStsCreateUpdate is used to associate an STS role with a given AWS accountID
func
(
b
*
backend
)
pathConfigStsCreateUpdate
(
req
*
logical
.
Request
,
data
*
framework
.
FieldData
)
(
*
logical
.
Response
,
error
)
{
accountID
:=
data
.
Get
(
"account_id"
)
.
(
string
)
if
accountID
==
""
{
return
logical
.
ErrorResponse
(
"missing AWS account ID"
),
nil
}
b
.
configMutex
.
Lock
()
defer
b
.
configMutex
.
Unlock
()
// Check if an STS role is already registered
stsEntry
,
err
:=
b
.
nonLockedAwsStsEntry
(
req
.
Storage
,
accountID
)
if
err
!=
nil
{
return
nil
,
err
}
if
stsEntry
==
nil
{
stsEntry
=
&
awsStsEntry
{}
}
// Check that an STS role has actually been provided
stsRole
,
ok
:=
data
.
GetOk
(
"sts_role"
)
if
ok
{
stsEntry
.
StsRole
=
stsRole
.
(
string
)
}
else
if
req
.
Operation
==
logical
.
CreateOperation
{
return
logical
.
ErrorResponse
(
"missing sts role"
),
nil
}
if
stsEntry
.
StsRole
==
""
{
return
logical
.
ErrorResponse
(
"sts role cannot be empty"
),
nil
}
// save the provided STS role
if
err
:=
b
.
nonLockedSetAwsStsEntry
(
req
.
Storage
,
accountID
,
stsEntry
);
err
!=
nil
{
return
nil
,
err
}
return
nil
,
nil
}
// pathConfigStsDelete is used to delete a previously configured STS configuration
func
(
b
*
backend
)
pathConfigStsDelete
(
req
*
logical
.
Request
,
data
*
framework
.
FieldData
)
(
*
logical
.
Response
,
error
)
{
b
.
configMutex
.
Lock
()
defer
b
.
configMutex
.
Unlock
()
accountID
:=
data
.
Get
(
"account_id"
)
.
(
string
)
if
accountID
==
""
{
return
logical
.
ErrorResponse
(
"missing account id"
),
nil
}
return
nil
,
req
.
Storage
.
Delete
(
"config/sts/"
+
accountID
)
}
const
pathConfigStsSyn
=
`
Specify STS roles to be assumed for certain AWS accounts.
`
const
pathConfigStsDesc
=
`
Allows the explicit association of STS roles to satellite AWS accounts (i.e. those
which are not the account in which the Vault server is running.) Login attempts from
EC2 instances running in these accounts will be verified using credentials obtained
by assumption of these STS roles.
The environment in which the Vault server resides must have access to assume the
given STS roles.
`
const
pathListStsHelpSyn
=
`
List all the AWS account/STS role relationships registered with Vault.
`
const
pathListStsHelpDesc
=
`
AWS accounts will be listed by account ID, along with their respective role names.
`
This diff is collapsed.
Click to expand it.
builtin/credential/aws-ec2/path_login.go
+
39
-
7
View file @
b548e286
...
...
@@ -79,12 +79,23 @@ needs to be supplied along with 'identity' parameter.`,
// instanceIamRoleARN fetches the IAM role ARN associated with the given
// instance profile name
func
(
b
*
backend
)
instanceIamRoleARN
(
s
logical
.
Storage
,
instanceProfileName
,
region
string
)
(
string
,
error
)
{
func
(
b
*
backend
)
instanceIamRoleARN
(
s
logical
.
Storage
,
instanceProfileName
,
region
,
accountID
string
)
(
string
,
error
)
{
if
instanceProfileName
==
""
{
return
""
,
fmt
.
Errorf
(
"missing instance profile name"
)
}
iamClient
,
err
:=
b
.
clientIAM
(
s
,
region
)
// Check if an STS configuration exists for the AWS account
sts
,
err
:=
b
.
lockedAwsStsEntry
(
s
,
accountID
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"error fetching STS config for account ID %q: %q
\n
"
,
accountID
,
err
)
}
// An empty STS role signifies the master account
stsRole
:=
""
if
sts
!=
nil
{
stsRole
=
sts
.
StsRole
}
iamClient
,
err
:=
b
.
clientIAM
(
s
,
region
,
stsRole
)
if
err
!=
nil
{
return
""
,
err
}
...
...
@@ -116,9 +127,21 @@ func (b *backend) instanceIamRoleARN(s logical.Storage, instanceProfileName, reg
// validateInstance queries the status of the EC2 instance using AWS EC2 API
// and checks if the instance is running and is healthy
func
(
b
*
backend
)
validateInstance
(
s
logical
.
Storage
,
instanceID
,
region
string
)
(
*
ec2
.
DescribeInstancesOutput
,
error
)
{
func
(
b
*
backend
)
validateInstance
(
s
logical
.
Storage
,
instanceID
,
region
,
accountID
string
)
(
*
ec2
.
DescribeInstancesOutput
,
error
)
{
// Check if an STS configuration exists for the AWS account
sts
,
err
:=
b
.
lockedAwsStsEntry
(
s
,
accountID
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"error fetching STS config for account ID %q: %q
\n
"
,
accountID
,
err
)
}
// An empty STS role signifies the master account
stsRole
:=
""
if
sts
!=
nil
{
stsRole
=
sts
.
StsRole
}
// Create an EC2 client to pull the instance information
ec2Client
,
err
:=
b
.
clientEC2
(
s
,
region
)
ec2Client
,
err
:=
b
.
clientEC2
(
s
,
region
,
stsRole
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -380,7 +403,7 @@ func (b *backend) pathLoginUpdate(
// Validate the instance ID by making a call to AWS EC2 DescribeInstances API
// and fetching the instance description. Validation succeeds only if the
// instance is in 'running' state.
instanceDesc
,
err
:=
b
.
validateInstance
(
req
.
Storage
,
identityDocParsed
.
InstanceID
,
identityDocParsed
.
Region
)
instanceDesc
,
err
:=
b
.
validateInstance
(
req
.
Storage
,
identityDocParsed
.
InstanceID
,
identityDocParsed
.
Region
,
identityDocParsed
.
AccountID
)
if
err
!=
nil
{
return
logical
.
ErrorResponse
(
fmt
.
Sprintf
(
"failed to verify instance ID: %v"
,
err
)),
nil
}
...
...
@@ -449,7 +472,7 @@ func (b *backend) pathLoginUpdate(
}
// Use instance profile ARN to fetch the associated role ARN
iamRoleARN
,
err
:=
b
.
instanceIamRoleARN
(
req
.
Storage
,
iamInstanceProfileName
,
identityDocParsed
.
Region
)
iamRoleARN
,
err
:=
b
.
instanceIamRoleARN
(
req
.
Storage
,
iamInstanceProfileName
,
identityDocParsed
.
Region
,
identityDocParsed
.
AccountID
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"IAM role ARN could not be fetched: %v"
,
err
)
}
...
...
@@ -622,6 +645,7 @@ func (b *backend) pathLoginUpdate(
Metadata
:
map
[
string
]
string
{
"instance_id"
:
identityDocParsed
.
InstanceID
,
"region"
:
identityDocParsed
.
Region
,
"account_id"
:
identityDocParsed
.
AccountID
,
"role_tag_max_ttl"
:
rTagMaxTTL
.
String
(),
"role"
:
roleName
,
"ami_id"
:
identityDocParsed
.
AmiID
,
...
...
@@ -745,8 +769,16 @@ func (b *backend) pathLoginRenew(
return
nil
,
fmt
.
Errorf
(
"unable to fetch region from metadata during renewal"
)
}
// Ensure backwards compatibility for older clients without account_id saved in metadata
accountID
,
ok
:=
req
.
Auth
.
Metadata
[
"account_id"
]
if
ok
{
if
accountID
==
""
{
return
nil
,
fmt
.
Errorf
(
"unable to fetch account_id from metadata during renewal"
)
}
}
// Cross check that the instance is still in 'running' state
_
,
err
:=
b
.
validateInstance
(
req
.
Storage
,
instanceID
,
region
)
_
,
err
:=
b
.
validateInstance
(
req
.
Storage
,
instanceID
,
region
,
accountID
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"failed to verify instance ID %q: %q"
,
instanceID
,
err
)
}
...
...
This diff is collapsed.
Click to expand it.
website/source/docs/auth/aws-ec2.html.md
+
158
-
0
View file @
b548e286
...
...
@@ -262,6 +262,20 @@ will not be aware of such events. The token issued will still be valid, until
it expires. The token will likely be expired sooner than its lifetime when the
instance fails to renew the token on time.
### Cross Account Access
To allow Vault to authenticate EC2 instances running in other accounts, AWS STS (Security
Token Service) can be used to retrieve temporary credentials by assuming an IAM Role
in those accounts.
The account in which Vault is running (i.e. the master account) must be listed as
a trusted entity in the IAM Role being assumed on the remote account. The Role itself
must allow the
`ec2:DescribeInstances`
action, and
`iam:GetInstanceProfile`
if IAM Role
binding is used (see below).
Furthermore, in the master account, Vault must be granted the action
`sts:AssumeRole`
for the IAM Role to be assumed.
## Authentication
### Via the CLI
...
...
@@ -609,6 +623,150 @@ The response will be in JSON. For example:
</dd>
</dl>
### /auth/aws-ec2/config/sts/<account_id>
#### POST
<dl
class=
"api"
>
<dt>
Description
</dt>
<dd>
Allows the explicit association of STS roles to satellite AWS accounts (i.e. those
which are not the account in which the Vault server is running.) Login attempts from
EC2 instances running in these accounts will be verified using credentials obtained
by assumption of these STS roles.
</dd>
<dt>
Method
</dt>
<dd>
POST
</dd>
<dt>
URL
</dt>
<dd>
`/auth/aws-ec2/config/certificate/<account_id>`
</dd>
<dt>
Parameters
</dt>
<dd>
<ul>
<li>
<span
class=
"param"
>
account_id
</span>
<span
class=
"param-flags"
>
required
</span>
AWS account ID to be associated with STS role. If set,
Vault will use assumed credentials to verify any login attempts from EC2
instances in this account.
</li>
</ul>
<ul>
<li>
<span
class=
"param"
>
sts_role
</span>
<span
class=
"param-flags"
>
required
</span>
AWS ARN for STS role to be assumed when interacting with the account specified.
The Vault server must have permissions to assume this role.
</li>
</ul>
</dd>
<dt>
Returns
</dt>
<dd>
`204`
response code.
</dd>
</dl>
#### GET
<dl
class=
"api"
>
<dt>
Description
</dt>
<dd>
Returns the previously configured STS role.
</dd>
<dt>
Method
</dt>
<dd>
GET
</dd>
<dt>
URL
</dt>
<dd>
`/auth/aws-ec2/config/sts/<account_id>`
</dd>
<dt>
Parameters
</dt>
<dd>
None.
</dd>
<dt>
Returns
</dt>
<dd>
```
javascript
{
"
auth
"
:
null
,
"
warnings
"
:
null
,
"
data
"
:
{
"
sts_role
"
:
"
arn:aws:iam:<account_id>:role/myRole
"
},
"
lease_duration
"
:
0
,
"
renewable
"
:
false
,
"
lease_id
"
:
""
}
```
</dd>
</dl>
#### LIST
<dl
class=
"api"
>
<dt>
Description
</dt>
<dd>
Lists all the AWS Account IDs for which an STS role is registered
</dd>
<dt>
Method
</dt>
<dd>
LIST/GET
</dd>
<dt>
URL
</dt>
<dd>
`/auth/aws-ec2/config/sts`
(LIST) or
`/auth/aws-ec2/config/sts?list=true`
(GET)
</dd>
<dt>
Parameters
</dt>
<dd>
None.
</dd>
<dt>
Returns
</dt>
<dd>
```
javascript
{
"
auth
"
:
null
,
"
warnings
"
:
null
,
"
data
"
:
{
"
keys
"
:
[
"
<account_id_1>
"
,
"
<account_id_2>
"
]
},
"
lease_duration
"
:
0
,
"
renewable
"
:
false
,
"
lease_id
"
:
""
}
```
</dd>
</dl>
#### DELETE
<dl
class=
"api"
>
<dt>
Description
</dt>
<dd>
Deletes a previously configured AWS account/STS role association
</dd>
<dt>
Method
</dt>
<dd>
DELETE
</dd>
<dt>
URL
</dt>
<dd>
`/auth/aws-ec2/config/sts/<account_id>`
</dd>
<dt>
Parameters
</dt>
<dd>
None.
</dd>
<dt>
Returns
</dt>
<dd>
`204`
response code.
</dd>
</dl>
### /auth/aws-ec2/config/tidy/identity-whitelist
##### POST
<dl
class=
"api"
>
...
...
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment
Menu
Projects
Groups
Snippets
Help