—package
Amazon::Credentials;
# - module for finding and providing AWS credentials
use
strict;
use
warnings;
use
5.010;
__PACKAGE__->follow_best_practice;
__PACKAGE__->mk_accessors(
qw(
_access_key_id
cache
cipher
container
debug
decrypt
encrypt
encryption
error
expiration
imdsv2
imdsv2_token
insecure
logger
order
no_passkey_warning
print_error
profile
raise_error
region
role
_secret_access_key
_session_token
session_token_required
source
sso_role_name
sso_account_id
sso_region
timeout
user_agent
)
);
use
Carp;
use
Config::Tiny;
use
Cwd;
use
Data::Dumper;
use
Date::Format;
use
Exporter;
use
File::HomeDir;
use
HTTP::Request;
use
LWP::UserAgent;
use
MIME::Base64;
use
Time::Local;
AWS_AVAILABILITY_ZONE_URL
=>
q{latest/meta-data/placement/availability-zone}
,
AWS_IAM_SECURITY_CREDENTIALS_URL
=>
q{latest/meta-data/iam/security-credentials/}
,
CACHE_DIR
=>
'.aws/sso/cache'
,
COMMA
=>
q{,}
,
DEFAULT_CIPHER
=>
q{Cipher::AES}
,
DEFAULT_REGION
=>
'us-east-1'
,
DEFAULT_SEARCH_ORDER
=>
q{env,container,role,file}
,
DEFAULT_TIMEOUT
=> 2,
DEFAULT_WINDOW_INTERVAL
=> 5,
EMPTY
=>
q{}
,
FALSE
=> 0,
GET_ROLE_CREDENTIALS_HOSTNAME
=>
'portal.sso.%s.amazonaws.com'
,
GET_ROLE_CREDENTIALS_QUERY
=>
'?account_id=%s&role_name=%s'
,
GET_ROLE_CREDENTIALS_URI
=>
'federation/credentials'
,
IMDSv2_DEFAULT_TTL
=> 21_600,
IMDSv2_HEADER
=>
q{x-aws-ec2-metadata-token}
,
IMDSv2_TTL_HEADER
=>
q{x-aws-ec2-metadata-token-ttl-seconds}
,
IMDSv2_URL
=>
q{latest/api/token}
,
INSECURE_MODE
=> 2,
ISO8601_FORMAT
=>
q{%Y-%m-%dT%H:%M:%S%z}
,
LOG_FORMAT
=>
qq{ %s [%s] %s\n }
,
PASSKEY_FORMAT
=>
q{%08X%08x}
,
RANDOM_VALUE
=> 0xffffffff,
SECONDS_IN_HOUR
=> 3600,
SECONDS_IN_MINUTE
=> 60,
SUCCESS
=> 0,
FAILURE
=> 1,
SLASH
=>
q{/}
,
TRUE
=> 1,
X_AMZ_SSO_BEARER_TOKEN
=>
':x-amz-sso_bearer_token'
,
};
our
$VERSION
=
'1.1.24'
;
## no critic (RequireInterpolation)
our
@EXPORT_OK
=
qw( create_passkey set_sso_credentials get_role_credentials )
;
# we only log at debug level, create a default logger
{
no
strict
'refs'
;
## no critic (ProhibitNoStrict)
*{
'Amazon::Credentials::Logger::debug'
} =
sub
{
my
(
$self
,
@message
) =
@_
;
return
if
!
$self
->{debug};
my
@tm
=
localtime
time
;
my
$timestamp
= strftime
'%c'
,
@tm
;
my
$message
=
sprintf
LOG_FORMAT,
$timestamp
,
$PROCESS_ID
,
@message
;
{
*STDERR
}
$message
;
};
}
caller
or __PACKAGE__->main();
########################################################################
sub
new {
########################################################################
my
(
$class
,
@args
) =
@_
;
my
$options
=
ref
$args
[0] ?
$args
[0] : {
@args
};
my
$passkey
=
delete
$options
->{passkey};
my
$self
=
$class
->SUPER::new(
$options
);
$self
->_set_defaults;
$self
->_init_logger;
# note that multiple instances of the Amazon::Credentials will share
# the same passkey...this is probably a bug
if
(
$self
->get_passkey &&
$passkey
) {
if
( !
$self
->get_no_passkey_warning ) {
carp
<<'WARNING';
WARNING: the encryption passkey has already been set. Resetting the
passkey will require that you restore the original passkey if you are
using more than one instance of Amazon::Credentials.
To turn this warning off set 'no_passkey_warning' to a true value.
WARNING
}
$self
->_init_encryption(
$passkey
);
}
elsif
(
$self
->get_passkey ) {
if
( !
$self
->get_no_passkey_warning ) {
carp
<<'WARNING';
WARNING: the encryption passkey has already been set. Using the previous passkey.
To turn this warning off set 'no_passkey_warning' to a true value.
WARNING
}
$self
->_init_encryption(
$self
->get_passkey );
}
else
{
$self
->_init_encryption(
$passkey
);
}
if
(
$self
->get_insecure ) {
$self
->get_logger->debug(
"!! CAUTION !!\n"
.
"!! You are executing in 'insecure' mode !!\n"
.
"!! Credentials may be exposed in debug messages !!\n"
);
}
# if the caller wants us to use his SSO credentials
if
(
$self
->get_sso_role_name &&
$self
->get_sso_account_id ) {
my
$region
=
$self
->get_sso_region ||
$self
->get_region;
# this just sets the environmet
set_sso_credentials(
$self
->get_sso_role_name,
$self
->get_sso_account_id,
$region
);
# we'll set them from the environment below
$self
->set_order( [
'env'
] );
}
if
( !
$options
->{aws_secret_access_key}
|| !
$options
->{aws_access_key_id} ) {
$self
->set_credentials;
if
(
$self
->get__session_token ) {
$self
->set_session_token_required(TRUE);
}
if
( !
$self
->get_cache ) {
$self
->reset_credentials;
}
}
else
{
$self
->get_logger->debug(
'setting credentials from options '
,
Dumper( [
$options
] ) );
$self
->set_credentials(
$options
);
}
if
( !
$self
->get_region ) {
$self
->set_region(
$self
->get_region_from_env
||
$self
->get_default_region );
}
return
$self
;
}
########################################################################
sub
set_default_logger {
########################################################################
my
(
$self
,
$debug
) =
@_
;
$debug
=
$debug
//
$self
->get_debug;
my
$logger
=
bless
{
debug
=>
$debug
},
'Amazon::Credentials::Logger'
;
$self
->set_logger(
$logger
);
return
$self
;
}
########################################################################
sub
reset_credentials {
########################################################################
my
(
$self
,
$renew
) =
@_
;
if
( !
$renew
) {
$self
->set__access_key_id(
undef
);
$self
->set__secret_access_key(
undef
);
$self
->set__session_token(
undef
);
}
else
{
if
(
$self
->get_cache ) {
$self
->set_credentials;
}
}
return
$self
;
}
########################################################################
sub
get_region_from_env {
########################################################################
my
(
$self
) =
@_
;
my
$region
=
$ENV
{AWS_REGION} ||
$ENV
{AWS_DEFAULT_REGION};
return
$region
;
}
########################################################################
sub
get_default_region {
########################################################################
my
(
$self
) =
@_
;
my
$region
=
$self
->get_region_from_config;
# only do this if we are sure we are on an EC2
if
(
$self
->get_source &&
$self
->get_source eq
'IAM'
) {
# try to get credentials from instance role, but we may not be
# executing on an EC2 or container
my
$url
;
if
(
$self
->get_container ) {
$url
=
"$ENV{ECS_CONTAINER_METADATA_URI_V4}/task"
;
}
else
{
$url
= _create_metadata_url(AWS_AVAILABILITY_ZONE_URL);
}
my
$ua
=
ref
$self
?
$self
->get_user_agent : LWP::UserAgent->new();
my
@headers
;
# add imdsv2 token to metadata request
if
( !
$self
->get_container &&
$self
->get_imdsv2_token ) {
@headers
= (
IMDSv2_HEADER
=>
$self
->get_imdsv2_token );
}
my
$req
= HTTP::Request->new(
GET
=>
$url
, \
@headers
);
$region
=
eval
{
my
$rsp
=
$ua
->request(
$req
);
# if not 200, then get out of Dodge
croak
"could not get availability zone\n"
if
!
$rsp
->is_success;
my
$content
=
$rsp
->content;
$content
=~ s/(\d+)[[:lower:]]+$/$1/xsm;
return
$content
;
};
}
return
$region
;
}
########################################################################
sub
set_credentials {
########################################################################
my
(
$self
,
$creds
) =
@_
;
$self
->set_error(EMPTY);
$creds
=
eval
{
if
( !
$creds
) {
$creds
=
eval
{
return
$self
->find_credentials };
}
$self
->set_error(
$creds
->{error} ||
$EVAL_ERROR
);
croak
'no credentials available'
if
!
$creds
->{aws_secret_access_key} || !
$creds
->{aws_access_key_id};
$self
->set_aws_secret_access_key(
$creds
->{aws_secret_access_key} );
$self
->set_aws_access_key_id(
$creds
->{aws_access_key_id} );
$self
->set_token(
$creds
->{token} ||
$creds
->{aws_session_token} );
$self
->set_expiration(
$creds
->{expiration} );
return
$creds
;
};
if
(
$EVAL_ERROR
&& !
$self
->get_error ) {
$self
->set_error(
$EVAL_ERROR
);
}
undef
$EVAL_ERROR
;
croak
$self
->get_error
if
$self
->get_error &&
$self
->get_raise_error;
carp
$self
->get_error
if
$self
->get_error &&
$self
->get_print_error;
return
$self
;
}
########################################################################
sub
get_ec2_credentials {
########################################################################
goto
&find_credentials
;
}
########################################################################
sub
find_credentials {
########################################################################
my
(
$self
,
@args
) =
@_
;
my
$options
=
ref
$args
[0] ?
$args
[0] : {
@args
};
if
(
$options
->{profile} ) {
$self
->set_profile(
$options
->{profile} );
}
if
(
$options
->{order} ) {
$self
->set_order(
$options
->{order} );
}
my
@search_order
;
if
(
$self
->get_profile ) {
@search_order
= (
'file'
);
}
elsif
(
ref
$self
->get_order && reftype(
$self
->get_order ) eq
'ARRAY'
) {
@search_order
= @{
$self
->get_order };
}
elsif
( !
ref
$self
->get_order ) {
@search_order
=
split
/\s*,\s*/xsm,
$self
->get_order;
}
$self
->get_logger->debug(
'search order '
.
join
COMMA,
@search_order
);
my
%creds_getters
= (
env
=>
sub
{
return
$self
->get_creds_from_env,;
},
role
=>
sub
{
return
$self
->get_creds_from_role;
},
container
=>
sub
{
return
$self
->get_creds_from_container;
},
file
=>
sub
{
return
$self
->get_creds_from_ini_file;
},
);
my
$creds
;
foreach
my
$location
(
@search_order
) {
$self
->get_logger->debug(
'searching for credentials in: '
.
$location
);
if
(
$creds_getters
{
$location
} ) {
$creds
=
$creds_getters
{
$location
}->();
}
last
if
$creds
->{source};
}
foreach
my
$k
(
keys
%{
$creds
} ) {
if
(
$k
!~ /^aws|token/xsm ) {
$self
->set(
$k
,
$creds
->{
$k
} );
}
elsif
(
$self
->can(
"set_$k"
) ) {
$self
->can(
"set_$k"
)->(
$self
,
$creds
->{
$k
} );
}
}
$self
->get_logger->debug(
$self
->safe_dumper(
$creds
) );
return
$creds
;
}
# +------------------+
# | get_creds_from_* |
# +------------------+
########################################################################
sub
get_creds_from_env {
########################################################################
my
(
$self
) =
@_
;
return
{}
if
!
$ENV
{AWS_ACCESS_KEY_ID} || !
$ENV
{AWS_SECRET_ACCESS_KEY};
$self
->get_logger->debug(
'fetching credentials from env'
);
my
@cred_keys
= (
aws_access_key_id
=>
'AWS_ACCESS_KEY_ID'
,
aws_secret_access_key
=>
'AWS_SECRET_ACCESS_KEY'
,
aws_session_token
=>
'AWS_SESSION_TOKEN'
,
token
=>
'AWS_SESSION_TOKEN'
,
);
return
populate_creds(
'ENV'
, \
@cred_keys
, \
%ENV
);
}
########################################################################
sub
populate_creds {
########################################################################
my
(
$source
,
$cred_keys
,
$creds_source
) =
@_
;
my
$creds
= {
source
=>
$source
};
foreach
my
$p
( pairs @{
$cred_keys
} ) {
my
(
$k
,
$v
) = @{
$p
};
$creds
->{
$k
} =
$creds_source
->{
$v
};
next
if
$source
=~ /env/i;
delete
$creds_source
->{
$v
};
}
return
$creds
;
}
########################################################################
sub
get_creds_from_process {
########################################################################
my
(
$self
,
$process
) =
@_
;
$self
->get_logger->debug(
"fetching credentials from $process"
);
my
$credentials
=
eval
{
open
my
$fh
,
q{-|}
,
$process
or croak
"could not open pipe to $process\n$OS_ERROR"
;
local
$RS
=
undef
;
my
$credentials_str
= <
$fh
>;
close
$fh
or croak
"could not close filehandle on $process\n"
;
return
decode_json(
$credentials_str
);
};
if
(
$EVAL_ERROR
|| !
$credentials
) {
croak
"could not get credentials from process\n$EVAL_ERROR\n"
;
}
$self
->get_logger->debug(
$self
->safe_dumper(
$credentials
) );
my
@cred_keys
= (
aws_access_key_id
=>
'AccessKeyId'
,
aws_secret_access_key
=>
'SecretAccessKey'
,
token
=>
'SessionToken'
,
aws_session_token
=>
'SessionToken'
,
region
=>
'Region'
,
expiration
=>
'Expiration'
,
);
return
populate_creds(
'process'
, \
@cred_keys
,
$credentials
);
}
########################################################################
sub
create_config_path {
########################################################################
my
(
$config
) =
@_
;
my
$fullpath
= home . SLASH .
$config
;
return
-e
$fullpath
?
$fullpath
: EMPTY;
}
########################################################################
sub
get_region_from_config {
########################################################################
my
(
$self
) =
@_
;
my
$config
= create_config_path(
'.aws/config'
);
return
if
!
$config
;
$self
->get_logger->debug(
"config: $config"
);
my
$ini
= Config::Tiny->
read
(
$config
);
my
$region
;
if
(
$ini
->{
default
} ) {
$region
=
$ini
->{
default
}->{region};
}
$self
->get_logger->debug(
'default region: '
.
$region
//
'undef'
);
return
$region
;
}
########################################################################
sub
get_creds_from_ini_file {
########################################################################
my
(
$self
,
$profile
) =
@_
;
$profile
=
$profile
||
$self
->get_profile ||
'default'
;
my
$creds
= {};
my
$region
;
foreach
my
$config
(
qw( .aws/config .aws/credentials )
) {
last
if
$creds
->{source};
my
$profile_name
=
$profile
;
my
$fullpath
= create_config_path(
$config
);
if
( !
$fullpath
) {
$self
->get_logger->debug(
'skipping '
.
$config
.
'...not found'
);
next
;
}
$self
->get_logger->debug(
'reading '
.
$config
);
my
$ini
= Config::Tiny->
read
(
$fullpath
);
$self
->get_logger->debug(
$self
->safe_dumper(
$ini
) );
if
(
$ini
->{
default
} ) {
$region
=
$ini
->{
default
}->{region};
}
my
$section
;
if
(
$ini
->{
$profile_name
} ) {
$section
=
$ini
->{
$profile_name
};
}
elsif
(
$ini
->{
"profile $profile_name"
} ) {
## config file format
$section
=
$ini
->{
"profile $profile_name"
};
}
my
$process
=
$ini
->{credential_process} ||
$section
->{credential_process};
if
(
$process
) {
$creds
=
$self
->get_creds_from_process(
$process
);
$region
=
$section
->{region} ?
$section
->{region} :
$region
;
}
elsif
(
$section
) {
# credentials in a config file shouldn't really have a session token,
# but if it does we'll capture it as aws_session_token. For
# historical reasons pertaining to how other CPAN classes
# referred to the token we store it internally as token
my
@cred_keys
= (
aws_access_key_id
=>
'aws_access_key_id'
,
aws_secret_access_key
=>
'aws_secret_access_key'
,
aws_session_token
=>
'aws_session_token'
,
token
=>
'aws_session_token'
,
region
=>
'region'
,
);
$creds
= populate_creds(
$config
, \
@cred_keys
,
$section
);
if
( !
$creds
->{aws_access_key_id} || !
$creds
->{aws_secret_access_key} )
{
$creds
= {};
}
}
}
$self
->set_region(
$creds
->{region} ||
$region
);
return
$creds
;
}
########################################################################
sub
get_creds_from_role {
########################################################################
my
(
$self
) =
@_
;
my
$aws_ec2_metadata_disabled
=
$ENV
{AWS_EC2_METADATA_DISABLED} // EMPTY;
if
(
$aws_ec2_metadata_disabled
eq
'true'
) {
$self
->get_logger->debug(
'AWS_EC2_METADATA_DISABLED is true, skipping...'
);
return
{};
}
# try to get credentials from instance role
my
$url
= _create_metadata_url(AWS_IAM_SECURITY_CREDENTIALS_URL);
my
$ua
=
$self
->get_user_agent;
my
$role
;
my
$creds
= {
error
=>
undef
};
if
(
$self
->get_imdsv2 ) {
my
$token_url
= _create_metadata_url(IMDSv2_URL);
my
@headers
= ( IMDSv2_TTL_HEADER, IMDSv2_DEFAULT_TTL );
my
$token_req
= HTTP::Request->new(
PUT
=>
$token_url
, \
@headers
);
$self
->get_logger->debug( Dumper
$token_req
);
my
$rsp
=
$ua
->request(
$token_req
);
$self
->get_logger->debug(
$self
->safe_dumper(
$rsp
) );
if
(
$rsp
->is_success ) {
$self
->set_imdsv2_token(
$rsp
->content );
}
else
{
croak
"could not retrieve IMDSv2 token\n"
;
}
}
my
@cred_keys
= (
aws_access_key_id
=>
'AccessKeyId'
,
aws_secret_access_key
=>
'SecretAccessKey'
,
token
=>
'Token'
,
aws_session_token
=>
'Token'
,
expiration
=>
'Expiration'
,
);
$creds
=
eval
{
# could be infinite, but I don't think so. Either we get an
# error ($@), or a non-200 response code
while
( !
$creds
->{token} ) {
if
(
$role
) {
$url
.=
$role
;
}
my
@headers
;
if
(
$self
->get_imdsv2 &&
$self
->get_imdsv2_token ) {
@headers
= ( IMDSv2_HEADER,
$self
->get_imdsv2_token );
}
my
$req
= HTTP::Request->new(
GET
=>
$url
, \
@headers
);
$self
->get_logger->debug( Dumper [
"HTTP REQUEST:\n"
,
$req
] );
my
$rsp
=
$ua
->request(
$req
);
$self
->get_logger->debug(
$self
->dump_response(
$rsp
) );
# if not 200, then get out of Dodge
last
if
!
$rsp
->is_success;
if
(
$role
) {
my
$creds_source
= decode_json(
$rsp
->content );
$creds
= populate_creds(
'IAM'
, \
@cred_keys
,
$creds_source
);
$creds
->{role} =
$role
;
}
else
{
$role
=
$rsp
->content;
$self
->get_logger->debug( Dumper [
'role'
,
$role
] );
last
if
!
$role
;
}
}
return
$creds
;
};
if
( !
$creds
||
$EVAL_ERROR
) {
$creds
->{error} =
$EVAL_ERROR
;
}
return
$creds
;
}
########################################################################
sub
get_creds_from_container {
########################################################################
my
(
$self
,
$uri
) =
@_
;
$uri
//=
$ENV
{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI};
my
$creds
= {};
if
( !
$uri
) {
$self
->get_logger->debug(
"not running in a container: no URI in environment\n"
);
return
$creds
;
}
$self
->get_logger->debug( Dumper( [
uri
=>
$uri
] ) );
$creds
=
eval
{
# try to get credentials from instance role
my
$url
= AWS_CONTAINER_CREDENTIALS_URL .
$uri
;
my
$ua
=
$self
->get_user_agent;
my
$req
= HTTP::Request->new(
GET
=>
$url
);
$req
->header(
qw( Accept */* )
);
$self
->get_logger->debug(
Dumper [
request
=>
$req
,
as_string
=>
$req
->as_string,
]
);
my
$rsp
=
$ua
->request(
$req
);
$self
->get_logger->debug(
$self
->dump_response(
$rsp
) );
# if not 200, then get out of Dodge
if
(
$rsp
->is_success ) {
my
$creds_source
= decode_json(
$rsp
->content );
my
@cred_keys
= (
aws_access_key_id
=>
'AccessKeyId'
,
aws_secret_access_key
=>
'SecretAccessKey'
,
token
=>
'Token'
,
aws_session_token
=>
'Token'
,
expiration
=>
'Expiration'
,
);
$creds
= populate_creds(
'IAM'
, \
@cred_keys
,
$creds_source
);
$creds
->{container} =
'ECS'
;
}
else
{
$self
->get_logger->debug(
'return code: '
.
$rsp
->status_line );
}
return
$creds
;
};
$creds
->{error} =
$EVAL_ERROR
;
croak
$EVAL_ERROR
if
$creds
->{error};
return
$creds
;
}
# +---------+
# | DUMPERS |
# +---------+
########################################################################
sub
safe_dumper {
########################################################################
my
(
$self
,
$obj
) =
@_
;
return
if
!
ref
$obj
;
my
$safe_rsp
;
if
(
$self
->get_insecure &&
$self
->get_insecure =~ /^2$/xsm ) {
$safe_rsp
= Dumper [
$obj
];
}
elsif
(
$self
->get_insecure ) {
$safe_rsp
= Dumper [
$obj
];
$safe_rsp
=~ s/(.*?)(access_?key[^
']+)'
([^
']+)'
([^
']+)'
/$1$2
'$3'
...'/ixsmg;
$safe_rsp
=~ s/(.*?)(secret_?access[^
']+)'
([^
']+)'
([^
']+)'
/$1$2
'$3'
...'/ixsmg;
$safe_rsp
=~ s/(.*?)(token[^
']+)'
([^
']+)'
([^
']+)'
/$1$2
'$3'
...'/ixsmg;
}
else
{
$safe_rsp
=
'** configuration contents blocked by insecure setting **'
;
}
return
$safe_rsp
;
}
########################################################################
sub
dump_response {
########################################################################
my
(
$self
,
$rsp
) =
@_
;
my
$safe_rsp
;
if
(
$self
->get_insecure &&
$self
->get_insecure =~ /^2$/xsm ) {
$safe_rsp
=
$rsp
;
}
elsif
(
$self
->get_insecure ) {
$safe_rsp
= {};
foreach
my
$k
(
keys
%{
$rsp
} ) {
if
(
$k
=~ /content/xsm ) {
my
$content
=
$rsp
->{
$k
};
$content
=~ s/\"(AccessKeyId|Token|SecretAccessKey)\"\s+\:\s+\"[^\"]+\"/\"$1\" : \"...\"/gxsm;
$safe_rsp
->{
$k
} =
$content
;
}
else
{
$safe_rsp
->{
$k
} =
$rsp
->{
$k
};
}
}
}
else
{
$safe_rsp
=
'** HTTP RESPONSE blocked by insecure setting **'
;
}
return
Dumper [
$safe_rsp
];
}
# +---------------+
# | TOKEN METHODS |
# +---------------+
########################################################################
sub
is_token_expired {
########################################################################
my
(
$self
,
$window_interval
) =
@_
;
$window_interval
=
$window_interval
// DEFAULT_WINDOW_INTERVAL;
my
$expiration_date
=
$self
->get_expiration();
my
$expired
= FALSE;
if
(
defined
$expiration_date
) {
# AWS recommends getting credentials 5 minutes prior to expiration
my
$g
= _iso8601_to_time(
$expiration_date
);
# shave 5 minutes or window interval off of the expiration time
$g
-=
$window_interval
* SECONDS_IN_MINUTE;
# (expiration_time - window_interval) - current_time = # of seconds left before expiration
my
$seconds_left
=
$g
-
time
;
if
(
$self
->get_debug ) {
$self
->get_logger->debug(
"seconds left : $seconds_left"
);
my
$hours
=
int
(
$seconds_left
/ SECONDS_IN_HOUR );
my
$minutes
=
int
(
(
$seconds_left
-
$hours
* SECONDS_IN_HOUR ) / SECONDS_IN_MINUTE );
my
$seconds
=
$seconds_left
- (
$hours
* SECONDS_IN_HOUR +
$minutes
* SECONDS_IN_MINUTE );
$self
->get_logger->debug(
"$hours hours $minutes minutes $seconds seconds until expiry"
);
}
$expired
= (
$seconds_left
< 0 ) ? TRUE : FALSE;
$self
->get_logger->debug(
Dumper [
expiration_date
=>
$expiration_date
,
expired
=>
$expired
]
);
}
return
$expired
;
}
########################################################################
sub
refresh_credentials {
########################################################################
goto
&refresh_token
;
}
########################################################################
sub
refresh_token {
########################################################################
my
(
$self
) =
@_
;
my
$creds
;
if
(
$self
->get_container &&
$self
->get_container eq
'ECS'
) {
$creds
=
$self
->get_creds_from_container;
}
elsif
(
$self
->get_role ) {
$creds
=
$self
->get_creds_from_role;
}
croak
'unable to refresh token!'
if
!
ref
$creds
|| !
keys
%{
$creds
};
return
$self
->set_credentials(
$creds
);
}
########################################################################
sub
credential_keys {
########################################################################
my
(
$self
) =
@_
;
my
%credential_keys
;
if
( !
$self
->get_cache ) {
my
$creds
=
$self
->find_credentials;
%credential_keys
= (
AWS_ACCESS_KEY_ID
=>
$creds
->{aws_access_key_id},
AWS_SECRET_ACCESS_KEY
=>
$creds
->{aws_secret_access_key},
AWS_SESSION_TOKEN
=>
$creds
->{token},
AWS_SESSION_TOKEN_EXPIRATION
=>
$creds
->{expiration},
);
}
else
{
%credential_keys
= (
AWS_ACCESS_KEY_ID
=>
$self
->get_aws_access_key_id,
AWS_SECRET_ACCESS_KEY
=>
$self
->get_aws_secret_access_key,
AWS_SESSION_TOKEN
=>
$self
->get_token,
AWS_SESSION_TOKEN_EXPIRATION
=>
$self
->get_expiration,
);
}
if
( !
defined
$credential_keys
{AWS_SESSION_TOKEN} ) {
delete
$credential_keys
{AWS_SESSION_TOKEN};
delete
$credential_keys
{AWS_SESSION_TOKEN_EXPIRATION};
}
return
\
%credential_keys
;
}
########################################################################
sub
as_string {
########################################################################
my
(
$self
) =
@_
;
return
JSON::PP->new->pretty->encode(
$self
->credential_keys );
}
########################################################################
sub
format_credentials {
########################################################################
my
(
$self
,
$format
) =
@_
;
$format
=
$format
||
"%s %s\n"
;
my
$credential_keys
=
$self
->credential_keys;
return
join
EMPTY,
map
{
sprintf
$format
,
$_
,
$credential_keys
->{
$_
} // EMPTY }
keys
%{
$credential_keys
};
}
# +----------------------------+
# | CREDENTIALS getters/setter |
# +----------------------------+
########################################################################
sub
get_aws_access_key_id {
########################################################################
my
(
$self
) =
@_
;
if
( !
$self
->get__access_key_id ) {
$self
->set_credentials;
}
my
$access_key_id
=
$self
->decrypt(
$self
->get__access_key_id,
$self
->_fetch_passkey );
if
( !
$self
->get_cache ) {
$self
->set__access_key_id(
undef
);
}
return
$access_key_id
;
}
########################################################################
sub
get_aws_secret_access_key {
########################################################################
my
(
$self
) =
@_
;
if
( !
$self
->get__secret_access_key ) {
$self
->set_credentials;
}
my
$secret_access_key
=
$self
->decrypt(
$self
->get__secret_access_key,
$self
->_fetch_passkey );
if
( !
$self
->get_cache ) {
$self
->set__secret_access_key(
undef
);
}
return
$secret_access_key
;
}
########################################################################
sub
get_token {
########################################################################
my
(
$self
) =
@_
;
if
( !
$self
->get__session_token &&
$self
->get_session_token_required ) {
$self
->set_credentials;
}
my
$passkey
=
$self
->_fetch_passkey;
my
$token
=
$self
->decrypt(
$self
->get__session_token,
$passkey
);
if
( !
$self
->get_cache ) {
$self
->set__session_token(
undef
);
}
return
$token
;
}
########################################################################
sub
set_aws_access_key_id {
########################################################################
my
(
$self
,
$aws_access_key_id
,
$passkey
) =
@_
;
my
$key
=
$aws_access_key_id
||
undef
;
if
(
$aws_access_key_id
) {
$key
=
$self
->encrypt(
$aws_access_key_id
,
$passkey
);
}
return
$self
->set__access_key_id(
$key
);
}
########################################################################
sub
set_aws_secret_access_key {
########################################################################
my
(
$self
,
$aws_secret_access_key
,
$passkey
) =
@_
;
my
$key
=
$aws_secret_access_key
||
undef
;
if
(
$aws_secret_access_key
) {
$key
=
$self
->encrypt(
$aws_secret_access_key
,
$passkey
);
}
return
$self
->set__secret_access_key(
$key
);
}
########################################################################
sub
set_aws_session_token {
goto
&set_token
; }
sub
get_aws_session_token {
goto
&get_token
; }
########################################################################
########################################################################
sub
set_token {
########################################################################
my
(
$self
,
$session_token
,
$passkey
) =
@_
;
my
$token
=
$session_token
||
undef
;
if
(
$session_token
) {
$token
=
$self
->encrypt(
$session_token
,
$passkey
);
}
return
$self
->set__session_token(
$token
);
}
# +--------------------+
# | ENCRYPTION METHODS |
# +--------------------+
########################################################################
sub
decrypt {
########################################################################
my
(
$self
,
$str
,
$passkey
) =
@_
;
if
(
ref
$self
->get_decrypt && reftype(
$self
->get_decrypt ) eq
'CODE'
) {
return
$self
->get_decrypt->(
$str
,
$passkey
||
$self
->_fetch_passkey );
}
else
{
return
$self
->_crypt(
$str
,
'decrypt'
,
$passkey
);
}
}
########################################################################
sub
encrypt {
########################################################################
my
(
$self
,
$str
,
$passkey
) =
@_
;
if
(
ref
$self
->get_encrypt && reftype(
$self
->get_encrypt ) eq
'CODE'
) {
return
$self
->get_encrypt->(
$str
,
$passkey
||
$self
->_fetch_passkey );
}
else
{
return
$self
->_crypt(
$str
,
'encrypt'
,
$passkey
);
}
}
########################################################################
sub
create_passkey {
########################################################################
return
sprintf
PASSKEY_FORMAT,
rand
RANDOM_VALUE,
rand
RANDOM_VALUE;
}
########################################################################
sub
rotate_passkey {
########################################################################
my
(
$self
,
$new_passkey
) =
@_
;
if
(
$new_passkey
&& !
ref
$new_passkey
) {
if
(
$self
->get_cache ) {
$self
->set_aws_access_key_id(
$self
->get_aws_access_key_id,
$new_passkey
);
$self
->set_aws_secret_access_key(
$self
->get_aws_secret_access_key,
$new_passkey
);
$self
->set_token(
$self
->get_token,
$new_passkey
);
}
# if caller has his own passkey generator, don't reset
if
( !
ref
$self
->get_passkey ) {
$self
->set_passkey(
$new_passkey
);
}
}
else
{
$new_passkey
=
$self
->create_passkey;
$self
->rotate_credentials(
$new_passkey
);
}
return
$new_passkey
;
}
########################################################################
sub
rotate_credentials {
########################################################################
goto
&rotate_passkey
;
}
########################################################################
sub
set_sso_credentials {
########################################################################
my
(
$role_name
,
$account_id
,
$region
) =
@_
;
croak
"usage: __PACKAGE__::set_sso_credentials(role-name, account-id)\n"
if
ref
$role_name
;
my
$credentials
= get_role_credentials(
role_name
=>
$role_name
,
account_id
=>
$account_id
,
region
=>
$region
// EMPTY,
);
my
@cred_keys
= (
accessKeyId
=>
'AWS_ACCESS_KEY_ID'
,
secretAccessKey
=>
'AWS_SECRET_ACCESS_KEY'
,
sessionToken
=>
'AWS_SESSION_TOKEN'
,
);
# stuff credentials in environment, upstream caller will set order to 'env'
if
(
$credentials
) {
foreach
my
$p
( pairs
@cred_keys
) {
my
(
$k
,
$v
) = @{
$p
};
$ENV
{
$v
} =
$credentials
->{
$k
};
## no critic (RequireLocalizedPunctuationVars)
}
}
return
$credentials
;
}
########################################################################
sub
get_role_credentials {
########################################################################
my
(
%args
) =
@_
;
my
(
$account_id
,
$role_name
,
$access_token
,
$region
)
=
@args
{
qw(account_id role_name access_token region)
};
if
( !
$access_token
) {
$access_token
= _get_access_token();
}
croak
'no access token'
if
!
$access_token
;
$region
||=
$ENV
{AWS_REGION} ||
$ENV
{AWS_DEFAULT_REGION} || DEFAULT_REGION;
my
$ua
= LWP::UserAgent->new;
$ua
->default_header( X_AMZ_SSO_BEARER_TOKEN,
$access_token
);
my
$host
=
sprintf
GET_ROLE_CREDENTIALS_HOSTNAME,
$region
;
my
$query
=
sprintf
GET_ROLE_CREDENTIALS_QUERY,
$account_id
,
$role_name
;
$query
;
my
$rsp
=
$ua
->get(
$url
);
croak
"no response from get($url)\n"
if
!
$rsp
;
my
$content
=
eval
{
return
decode_json(
$rsp
->content )
if
$rsp
&&
$rsp
->content_type eq
'application/json'
;
};
croak
sprintf
"could not decode response from GetRoleCredentials\n%s"
,
$EVAL_ERROR
if
!
$content
;
return
$content
->{roleCredentials}
if
$rsp
->is_success;
croak
sprintf
"Status: %s\n%s"
,
$rsp
->status_line, Dumper(
$rsp
->content )
if
!
ref
$content
;
croak
sprintf
"Status: %s\n%s"
,
$rsp
->status_line,
$content
->{message};
}
# +-----------------+
# | PRIVATE METHODS |
# +-----------------+
########################################################################
sub
_init_logger {
########################################################################
my
(
$self
) =
@_
;
if
( !
$self
->get_logger || !
ref
$self
->get_logger ) {
$self
->set_default_logger;
}
$self
->get_logger->debug(
'using '
.
ref
(
$self
->get_logger ) .
' logger'
);
return
$self
;
}
########################################################################
sub
_set_defaults {
########################################################################
my
(
$self
) =
@_
;
$self
->set_debug(
$self
->get_debug // FALSE );
$self
->set_cache(
defined
$self
->get_cache ?
$self
->get_cache : TRUE );
if
( !
$self
->get_user_agent ) {
# set a very low timeout
$self
->set_user_agent(
LWP::UserAgent->new(
timeout
=>
$self
->get_timeout || DEFAULT_TIMEOUT )
);
}
my
$default_search_order
= [
split
/\s*,\s*/xsm, DEFAULT_SEARCH_ORDER ];
if
( !
$self
->get_order ) {
$self
->set_order(
$default_search_order
);
}
if
( !
ref
$self
->get_order ) {
$self
->set_order( [
split
/\s*,\s*/xsm,
$self
->get_order ] );
}
elsif
(
ref
$self
->get_order && reftype(
$self
->get_order ) ne
'ARRAY'
) {
croak
'order must be a comma delimited string or array ref'
;
}
foreach
my
$loc
( @{
$self
->get_order } ) {
croak
"invalid credential location in search order: [$loc]"
if
!any {/^
$loc
$/xsm} @{
$default_search_order
};
}
if
( !
$self
->get_profile && any {/file/xsm} @{
$self
->get_order } ) {
$self
->set_profile(
$ENV
{AWS_PROFILE} );
}
$self
->set_raise_error(
$self
->get_raise_error // TRUE );
$self
->set_print_error(
$self
->get_print_error // TRUE );
return
$self
;
}
########################################################################
# note that $passkey is a class variable and as such, once initialized
# in the fashion below, will persist for all instances of
# Amazon::Credentials
########################################################################
{
my
$passkey
;
########################################################################
sub
get_passkey {
########################################################################
return
$passkey
;
}
########################################################################
sub
set_passkey {
########################################################################
my
(
$self
,
$key
) =
@_
;
$passkey
=
$key
;
return
$passkey
;
}
}
########################################################################
sub
_fetch_passkey {
########################################################################
my
(
$self
) =
@_
;
my
$passkey
=
eval
{
if
(
ref
$self
->get_passkey && reftype(
$self
->get_passkey ) eq
'CODE'
) {
return
$self
->get_passkey->();
}
else
{
return
$self
->get_passkey;
}
};
return
$passkey
;
}
########################################################################
sub
_init_encryption {
########################################################################
my
(
$self
,
$passkey
) =
@_
;
$self
->set_passkey(
$passkey
);
# if one is set, both must be set
if
(
$self
->get_encrypt ||
$self
->get_decrypt ) {
croak
'must be a code reference to encrypt()'
if
ref
$self
->get_encrypt ne
'CODE'
;
croak
'must be a code reference to decrypt()'
if
ref
$self
->get_decrypt ne
'CODE'
;
$self
->set_encryption(TRUE);
}
else
{
my
$has_crypt_cbc
=
eval
{
};
if
( !
defined
$self
->get_encryption ) {
# let's make the default to encrypt (if we can)
$self
->set_encryption(
$has_crypt_cbc
? TRUE : FALSE );
}
else
{
# don't allow encryption if Crypt::CBC not present
$self
->set_encryption(
$self
->get_encryption &&
$has_crypt_cbc
);
}
if
(
$self
->get_encryption && !
$self
->get_cipher ) {
$self
->set_cipher(DEFAULT_CIPHER);
}
}
if
(
$self
->get_encryption && !
$self
->get_passkey ) {
$self
->set_passkey(
$self
->create_passkey );
}
return
$self
->get_encryption;
}
########################################################################
sub
_crypt {
########################################################################
my
(
$self
,
$str
,
$op
,
$passkey
) =
@_
;
return
if
!
$str
;
$passkey
=
$passkey
||
$self
->_fetch_passkey();
my
$cipher
;
if
(
$self
->get_encryption ) {
$cipher
= Crypt::CBC->new(
'-pass'
=>
$passkey
,
'-key'
=>
$passkey
,
'-cipher'
=>
$self
->get_cipher,
'-nodeprecate'
=> TRUE,
);
}
# at least obfuscate the credentials
if
(
$op
eq
'decrypt'
) {
$str
= decode_base64(
$str
);
$str
=
ref
$cipher
?
$cipher
->decrypt(
$str
) :
$str
;
}
else
{
$str
=
ref
$cipher
?
$cipher
->encrypt(
$str
) :
$str
;
$str
= encode_base64(
$str
);
}
return
$str
;
}
########################################################################
sub
_iso8601_to_time {
########################################################################
my
$iso8601
=
shift
;
$iso8601
=~ s/^(.*)Z$/$1\+00:00/xsm;
my
$gmtime
=
eval
{
local
$ENV
{TZ} =
'GMT'
;
timegm( strptime(
$iso8601
, ISO8601_FORMAT ) );
};
return
$gmtime
;
}
########################################################################
sub
_create_metadata_url {
########################################################################
my
(
$url
) =
@_
;
return
AWS_METADATA_BASE_URL .
$url
;
}
########################################################################
sub
_get_access_token {
########################################################################
my
(
$home
) =
@_
;
File::Find->
import
(
'find'
);
$home
//=
$ENV
{HOME};
my
$cache_dir
=
sprintf
'%s/%s'
,
$home
, CACHE_DIR;
croak
"no $cache_dir found."
if
!-d
$cache_dir
;
my
$access_token
;
my
$cwd
= getcwd;
eval
{
find(
sub
{
return
if
!/[.]json$/xsm;
local
$RS
=
undef
;
open
my
$fh
,
'<'
,
$File::Find::name
or croak
'could not open '
.
$File::Find::name
;
my
$json
=
eval
{ decode_json(<
$fh
>) };
close
$fh
;
if
(
ref
$json
&&
$json
->{accessToken} ) {
$access_token
=
$json
->{accessToken};
croak
'found'
;
}
return
;
},
$cache_dir
);
};
CORE::
chdir
$cwd
;
return
$access_token
;
}
#######################################################################
sub
help {
########################################################################
my
(
$options
) =
@_
;
if
( !
$options
->{version} ) {
{
*STDOUT
}
<<'END_OF_USAGE';
amazon-credentials options
Formats credentials found in env, config, SSO, role
Options
-------
--account, -a use with --role, specify AWS account id
--container, -c get credentials from container IAM role
--ec2, -E get credentials from EC2 IAM role
--env -e get credentials from environment variables
--help, -h this
--profile, -p get credentials from profile in credentials configuration
--role, -r get credentials from SSO role
--version, -v version
$ amazon-credentials --profile=test
export AWS_ACCESS_KEY_ID=AKI*****************
export AWS_SECRET_ACCESS_KEY=****************************************
$ aws sso login
$ amazon-credentials --role my-sso-role --account 01234567890
END_OF_USAGE
}
{
*STDOUT
}
"v$VERSION\n"
;
exit
SUCCESS;
}
########################################################################
sub
export_credentials {
########################################################################
my
(
$credentials
) =
@_
;
$credentials
//= \
%ENV
;
my
@cred_keys
=
qw(AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN)
;
@cred_keys
=
map
{
defined
$credentials
->{
$_
} ?
$_
: () }
@cred_keys
;
return
join
"\n"
,
map
{
sprintf
'export %s=%s'
,
$_
,
$credentials
->{
$_
} }
@cred_keys
;
}
########################################################################
sub
main {
########################################################################
my
(
$self
) =
@_
;
my
%options
;
my
@option_specs
=
qw(
account|a=s
container|c
ec2|E
env|e
help
profile|p=s
role|r=s
version
)
;
if
( !GetOptions( \
%options
,
@option_specs
)
||
$options
{help}
||
$options
{version} ) {
help( \
%options
);
}
if
(
$options
{role} &&
$options
{account} ) {
my
$credentials
= Amazon::Credentials::set_sso_credentials(
@options
{
qw(role account region)
} );
{
*STDOUT
} export_credentials(
$credentials
);
exit
SUCCESS;
}
my
@order
=
eval
{
return
'env'
if
$options
{env};
return
'role'
if
$options
{ec2};
return
'container'
if
$options
{container};
return
'file'
if
$options
{profile};
return
split
/,/xsm, DEFAULT_SEARCH_ORDER;
};
my
$creds
=
$self
->new(
profile
=>
$options
{profile} // EMPTY,
order
=> [
@order
],
);
{
*STDOUT
}
$creds
->format_credentials(
"export %s=%s\n"
);
exit
SUCCESS;
}
1;
__END__
=pod
=head1 NAME
Amazon::Credentials - fetch Amazon credentials from file, environment or role
=head1 SYNOPSIS
my @order = qw( env file container role );
my $creds = Amazon::Credentials->new( { order => \@order } );
CLI
amazon-credentials --help
=head1 DESCRIPTION
Class to find AWS credentials from either the environment,
configuration files, instance meta-data or container role.
You can specify the order using the C<order> option in the constructor
to determine the order in which the class will look for credentials.
The default order is I<environent>, I<file>, I<container>, I<instance
meta-data>. See L</new>.
I<NEW!>
This class also implements a method for retrieving your SSO
credentials. By default the method will set the environment variables
C<AWS_ACCESS_KEY_ID>, C<AWS_SECRET_ACCESS_KEY> and
C<AWS_SESSION_TOKEN>. Subsequently call L<Amazon::Credentials> to
retrieve and use the credentials from the localized environment. If
you only want to retrieve the credentials use C<get_role_credentials>.
use Amazon::Credentials qw(set_sso_credentials get_role_credentials);
set_sso_credentials($role_name, $account_id, $region);
my $credentials = Amazon::Credentials->new;
my $credential = get_role_credentials(role_name => $sso_role_name,
account_id => $sso_account_id,
region => $sso_region);
or from the command line...
amazon-credentials.sh --role my-sso-role --account 01234567890
or pass your SSO role name and account ID...
my $credentials = Amazon::Credentials->new(sso_role_name => $role,
sso_account_id => $account_id,
sso_region => $region,
);
=head1 VERSION
This document reverse to verion 1.1.24 of
L<Amazon::Credentials>.
=head1 METHODS AND SUBROUTINES
=head2 new
new( options );
my $aws_creds = Amazon::Credential->new( { profile => 'sandbox', debug => 1 });
C<options> is a hash of keys that represent various options you can
pass to the constructor to control how it will look for credentials.
Any of the options can also be retrieved using their corresponding
'get_{option} method.
=head3 options
=over 5
=item aws_access_key_id
AWS access key.
=item aws_secret_access_key
AWS secret access key.
I<Note: If you pass the access keys in the constructor then the
constructor will not look in other places for credentials.>
=item cache
boolean when set to false will prevent L<Amazon::Credentials> from
cacheing credentials. B<Cacheing is enabled by default.>
I<Note that the if cacheing is disabled, the module will obtain
credentials on the first call to one of the getters
(C<get_aws_secret_access_key>, C<get_aws_access_key_id> or
C<get_token>). After each method call to retrieve the credential it
will be removed. However, for a brief period before all of them have
been accessed by the getter credentials will be locally stored.>
If you use the C<credential_keys> method for retrieving credentials,
the entire tuple of credentials will be immediately passed to you
without cacheing (if cacheing is disabled).
=item container
If the process is running in a container, this value will contain
'ECS' indicating that the credentials were optained for the task
role. The class will look for credentials using the container metadata
service:
http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
=item debug
Set to true for verbose troubleshooting information. Set C<logger> to
a logger that implements a logging interface (ala
L<Log::Log4perl>.
=item decrypt
Reference to a custom method that will decrypt credentials prior to
returning them from the cache. The method will be passed the string to
decrypt and a passkey.
=item encrypt
Reference to a custom method that will encrypt credentials prior to
storing them in the cache. The method will be passed a string to
encrypt and the passkey.
=item env - Environment
If there exists an environment variable $AWS_PROFILE, then an attempt
will be made to retrieve credentials from the credentials file using
that profile, otherwise the class will for these environment variables
to provide credentials.
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_SESSION_TOKEN
I<Note that when you set the environment variable AWS_PROFILE, the
order essentially is overridden and the class will look in your
credential files (F<~/.aws/config>, F<~/.aws/credentials>) to resolve
your credentials.>
=item file - Configuration Files
=over 10
=item ~/.aws/config
=item ~/.aws/credentials
=back
The class will attempt to resolve credentials by interpretting the
information in these two files. You can also specify a profile to use
for looking up the credentials by passing it into the constructor or
setting it the environment variable C<AWS_PROFILE>. If no profile is
provided, the default credentials or the first profile found is used.
my $aws_creds = Amazon::Credentials->new({ order => [qw/environment role file/] });
=item insecure
A debugging mode can be enabled to display information that may aid in
troubleshooting, however output may include credentials. This
attribute prevents accidental exfiltration of credentials during
troubleshooting. The default setting of C<insecure> is therefore
C<false>. This will prevent debug messages that may contain credentials
(HTTP response, configuration file contents) from exposing sensitive
data.
Set the value to 1 to enable all debug output B<except> the content of
credentials in HTTP responses. Set the value to 2 to enable full debug
output.
I<Note that setting the value to 1 will enable the use of regular
expressions to suppress credential contents. Credentials that do not
conform to these may still be exposed. Caution is advised.>
=item logger
Pass in your own logger that has a C<debug()> method. Otherwise the
default logger will output debug messages to STDERR.
=item no_passkey_warning
Boolean that indicates whether warning messages about passkey usage
should be supressed.
If you attempt to reset the passkey or if you instantiate a second
instance of Amazon::Credentials, the constructor will issue warnings.
Resetting a passkey means that you previous version of
Amazon::Credentials will no longer be able to decrypt credentials
unless you restore the original passkey.
If you instantiate another version of Amazon::Credentials without
resetting the passkey, the new instance will use the old value for the
passkey. This is by design.
default: false
=item order
An array reference containing tokens that specifies the order in which the class will
search for credentials.
default: env, role, container, file
Example:
my $creds = Amazon::Credentials->new( { order => [ qw/file env role/] });
=item passkey
A custom passkey for encryption. You can pass a scalar or a reference
to a subroutine that returns the passkey. The return value of the
subroutine should be idempotent, however you can change the subroutine
used for encryption if you are B<not> cacheing the credentials. If
you are cacheing credentials you should reset the credentials with the
new passkey method.
$credentials->set_passkey(\&new_passkey_provider);
$credentials->reset_credentials(1);
=item print_error
Whether to print the error if no credenials are found. C<raise_error>
implies C<print_error>.
default: true
=item profile
The profile name in the configuration file (F<~/.aws/config> or
F<~/.aws/credentials>).
my $aws_creds = Amazon::Credentials->new({ profile => 'sandbox' });
The class will also look for the environment variable C<AWS_PROFILE>,
so you can invoke your script like this:
$ AWS_PROFILE=sandbox my-script.pl
=item raise_error
Whether to raise an error if credentials are not found.
default: true
=item region
Default region. The class will attempt to find the region in either
the configuration files or the instance unless you specify the region
in the constructor.
=item role - Instance Role
The class will use the
to look for an instance role and credentials.
Credentials returned by accessing the meta-data include a token that
should be passed to Amazon APIs along with the access key and secret.
That token has an expiration and should be refreshed before it
expires.
if ( $aws_creds->is_token_expired() ) {
$aws_creds->refresh_token()
}
=item timeout
When looking for credentials in metadata URLs, this parameter
specifies the timeout value for L<LWP>.
default: 3s
=item user_agent
Pass in your own user agent, otherwise LWP will be used. I<Probably>
only useful to override this for testing purposes.>
=back
=head2 as_string
as_string()
Returns the credentials as a JSON encode string.
=head2 credential_keys
my $credential_keys = $creds->credential_keys;
Return a hash reference containing the credential keys with standard
key names. Note that the session token will only be present in the
hash for temporary credentials.
=over 5
=item AWS_ACCESS_KEY_ID
=item AWS_SECRET_ACCESS_KEY
=item AWS_SESSION_TOKEN
=back
=head2 format_credentials
format_credentials(format-string)
Returns the credentials as a formatted string. The <format> argument
allows you to include a format string that will be used to output each
of the credential parts.
format("export %s=%s\n");
The default format is a "%s %s\n".
=head2 find_credentials
find_credentials( option => value, ...);
You normally don't want to use this method. It's automatically invoked
by the constructor if you don't pass in any credentials. Accepts a
hash or hash reference consisting of keys (C<order> or C<profile>) in
the same manner as the constructor.
=head2 get_creds_from_*
These methods are called internally when the C<new> constructor is
invoked. You should never need to call these methods. All of these
methods will return a hash of credential information and metadata
described below.
=over 5
=item aws_access_key_id
The AWS access key.
=item aws_secret_access_key
The AWS secret key.
=item token
Security token used with access keys.
=item expiration
Token expiration date.
=item role
IAM role if available.
=item source
The source from which the credentials were found.
=over 3
=item * IAM - retrieved from container or instance role
=item * container - 'ECS' if retrieved from container
=item * file - retrieved from file
=item * process - retrieved from an external process
=item * ENV - retrieved from environment
=back
=back
=head3 get_creds_from_container
get_creds_from_container()
Retrieves credentials from the container's metadata at
http://169.254.170.2. Returns a hash of credentials containing:
aws_access_key_id
aws_secret_access_key
aws_session_token
Returns an empty hash if no credentials found. The environment
variable C<AWS_CONTAINER_CREDENTIALS_RELATIVE_URI> must exist or you
must pass the value of the path as an argument.
=head3 get_creds_from_process
get_creds_from_process(process)
Retrieves credentials from a helper process defined in the config
file. Returns the credentials tuple.
=head3 get_creds_from_role
get_creds_from_role()
Returns a hash, possibly containing access keys and a token.
=head2 get_default_region
Returns the region of the currently running instance or container.
The constructor will set the region to this value unless you set your
own C<region> value. Use C<get_region> to retrieve the value after
instantiation or you can call this method again and it will make a
second call to retrieve the instance metadata.
=head2 get_ec2_credentials (deprecated)
See L</find_credentials>
=head2 is_token_expired
is_token_expired( window-interval )
Returns true if the token is about to expire (or is
expired). C<window-interval> is the time in minutes before the actual
expiration time that the method should consider the token expired.
The default is 5 minutes. Amazon states that new credentials will be
available I<at least> 5 minutes before a token expires.
=head2 reset_credentials
By default this method will remove credentials from the cache if you
pass a false or no value. Passing a true value will refresh your
credentials from the original source (equivalent to calling
C<set_credentials>).
=head2 refresh_token (deprecated)
use C<refresh_credentials()>
=head2 refresh_credentials()
Retrieves a fresh set of IAM credentials.
if ( $creds->is_token_expired ) {
$creds->refresh_token()
}
=head2 set_credentials
Looks for your credentials according to the order specified by the
C<order> attribute passed in the constructor and stores the
credentials in the cache.
I<Note that you should never have to call
this method. If you call this method it will ignore your cache
setting!>
=head1 SSO CREDENTIALS
You can retrieve your SSO credentials after logging in using the
C<sso_set_credentials> or C<get_role_credentials> methods.
After logging in using your SSO credentials...
aws sso login
...call one of the methods below to retrieve your credentials.
=head2 get_role_credentials
get_role_credentials( options )
C<options> is a hash (not reference) of options
=over 5
=item role_name => role name (required)
=item account_id => AWS account id (required)
=item region => AWS region where SSO has been provisioned
default: $ENV{AWS_REGION}, $ENV{AWS_DEFAULT_REGION}, us-east-1
=back
=head2 set_sso_credentials
set_sso_options(role-name, account-id, region)
Calls C<get_role_credentials> and set AWS credenital environment
variables. Region is optional, all other parameters are required.
use Amazon::Credentials qw(set_sso_credentials)
set_sso_credentials(@ENV{qw(AWS_ROLE_NAME AWS_ACCOUNT_ID)});
my $credentials = Amazon::Credentials->new;
=head1 SETTERS/GETTERS
All of the options described in the new method can be accessed by a
I<getter> or set using a I<setter> of the same name.
Example:
$creds->set_cache(0);
=head1 DIAGNOSTICS
Set the C<debug> option when you instantiate a L<Amazon::Credentials>
object to output debug and diagnostic messages. Note that you must
also set the C<insecure> option if you want to output full
diagnostics. I<WARNING: Full diagnostics may include credentials. Be
careful not to expose these values in logs.>
=head1 CONFIGURATION AND ENVIRONMENT
The module will recognize several AWS specific environment variables
described throughout this documentation.
=over 5
=item AWS_ACCESS_KEY_ID
=item AWS_SECRET_ACCESS_KEY
=item AWS_SESSION_TOKEN
=item AWS_REGION
=item AWS_DEFAULT_REGION
=item AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
=back
=head1 BUGS AND LIMITATIONS
L<Amazon::Credentials> will B<not> attempt to retrieve temporary
credentials for profiles that specify a role. If for example you
define a role in your credentials file thusly:
[developer]
role_arn = arn:aws:iam::123456789012:role/developer-access-role
source_profile = dev
The module will not return credentials for the I<developer>
profile. While it would be theoretically possible to return those
credentials, in order to assume a role, one needs credentials (chicken
and egg problem).
=head1 DEPENDENCIES
Lower versions of these modules may be acceptable.
'Class::Accessor::Fast' => '0.31'
'Config::Tiny' => '2.28'
'Date::Format' => '2.24'
'File::HomeDir' => '1.00'
'File::chdir' => '0.1010'
'HTTP::Request' => '6.00'
'List::Util' => '1.5'
'LWP::UserAgent' => '6.36'
'POSIX::strptime' => '0.13'
...and possibly others
In order to enable true encryption of your credentials when cached,
L<Crypt::CBC> is also required.
=head1 SECURITY CONSIDERATIONS
The security concern around your credentials is not actually the fact
that the credentials can be retrieved and viewed - any process that
compromises your environment can use the same methods this class does
to resolve those credentials. Let me repeat that. If your environment
is compromised then an actor can use all of the methods employed in
this module to access your credentials.
The major issue you should be concerned about is exposing your
credentials outside of the environment running your program. Thats
is, the exfiltration of your credentials. Once you have resolved
these credentials you may inadvertantly reveal them in many
ways. Dumping objects to logs, saving your credentials in files or
even outputing them to your console may expose your credentials. This
module will now at the very least obfuscate them when they are stored
in memory. Accidental dumping of objects will not reveal your
credentials in plain-text.
B<Always take precautions to prevent accidental exfiltration of your
credentials.>
=head2 How L<Amazon::Credentials> Helps Prevent Exfiltration
For performance and historical reasons the default is for
L<Amazon::Credentials> to cache your credentials. Starting with
version I<1.1.0>, the module will attempt to encrypt the credentials
before storing them. The module uses L<Crypt::CBC> (if available) with
the default cipher and a random (or user defined) passkey.
Even if L<Crypt::CBC> is not available, the module will try to
obfuscate the credentials. A determined actor can still decrypt these
keys if they have access to the obfuscated values and your
passkey. You have several options to better secure your credentials
from exposure.
=over 5
=item Option 1 - Do not cache your credentials.
Use the C<set_cache()> method with a false value or set C<cache> to
false when you instantiate the class. B<The default is to cache
credentials.>
my $credentials = Amazon::Credentials->new(cache => 0);
Normally, your credentials are fetched when the L<Amazon::Credentials>
object is instantiated. With cacheing turned off credentials will not
be fetched until they are first requested.
There are two ways your programs typically will fetch the keys; either
using the getter methods on the individual credentials keys or by
retrieving a hash containing all of the keys.
=over 5
=item C<credential_keys()>
Use the method C<credential_keys> to retrieve all of the keys at once
as a hash. Using this method with cacheing turned off will prevent
L<Amazon::Credentials> from ever saving your credentials to variables
that can be inadvertantly exposed. Each subsequent request for the
keys will cause L<Amazon::Credentials> to fetch the keys again.
=item Getter Methods
If you use the individual getters (C<get_aws_access_key_id>,
C<get_aws_secret_access_key> and C<get_token>), the keys will first be
fetched and stored. As each getter is called the key will be removed
(burn after reading, so to speak). Therefore, for a brief period your
credentials will be cached even if cacheing is turned off.
=back
=item Option 2 - Remove them manually after use
Call the C<reset_credentials()> with a false value after
fetching credentials or after they are used by downstream
processes. Call the C<reset_credential()> method with a true value to
regenerate credentials.
=item Option 3 - Encrypt your credentials
L<Amazon::Credentials> will encrypt your credentials by default
starting with version I<1.1.0>. If L<Crypt::CBC> is available, the
class will use the default cipher and a random passkey to encrypt your
credentials. If the encryption module is not available, the class will
still obfuscate (not encrypt) the credentials. Encryption when the
passkey and method used are known to a determined bad actor is
no better than obfuscation. Accordingly, there are several ways you
can and should encrypt credentials in a more secure way.
=over 3
=item Using a Custom C<passkey>
By default the module will generate its own random passkey during
initialization and use that to encrypt and decrypt the
credentials. Obviously the passkey must be available for
L<Amazon::Credentials> to decrypt the keys, however it is B<NOT>
stored in the blessed hash reference that stores other data used by
the class. Instead the passkey is a class variable and will be
initialized once for all instances of L<Amazon::Credentials> your
script uses.
If you plan on using multiple instances of L<Amazon::Credentials> B<and>
you are passing in your own passkeys, then you'll need to reset the
passkey for each use of the credentials. See the example below in the
L</Using Multiple Instances of Amazon::Credentials> section.
To avoid having the class know about your passkey at all, pass a
reference to a subroutine that will provide the passkey for encryption
and decryption. You can even use the same passkey generator that is
used by L<Amazon::Credentials> (C<create_passkey>).
The point here is to avoid storing your passkey in the same object as
the credentials to minimize the likelihood of exposing your
credentials or your methods for encryption in logs...better but not
perfect. It's still may be possible to expose your passkey and your
credentials if you are not careful.
use Amazon::Credentials qw( create_passkey );
my $passkey = create_passkey();
my $credentials = Amazon::Credentials->new(
passkey => sub { return caller(0) eq 'Amazon::Credentials' && $passkey },
);
A more secure approach would be for your subroutine to retrieve a
passkey from a source other than your own program and B<never> store
the passkey inside your program.
=item Using Multiple Instances of Amazon::Credentials
You may at times need to assume a role using initial credentials. In
this case you can use multiple instances of
L<Amazon::Credentials>. Let's suppose that you have logged in with
your SSO credentials but your script must assume a role in another
account to perform some action.
# 1. retrieve SSO credentials
my $sso_credentials = Amazon::Credentials->new(
sso_role_name => 'developer',
sso_account_id => '01234567890'
);
# 2. assume a role in another account
my $role_arn = 'arn:aws:iam::09876543210:role/Route53AccountAccessRole';
my $role_session_name = "route53-role-$PID";
# using the SSO credentials which presumably allow you to assume the role...
my $sts = Amazon::API::STS->new( credentials => $sso_credentials );
my $assume_role_result = $sts->AssumeRole(
{ RoleArn => $role_arn,
RoleSessionName => $role_session_name,
}
);
my $assume_role_credentials = $assume_role_result->{AssumeRoleResult}->{Credentials};
# 3. create new credentials for assumed role
my $role_credentials = Amazon::Credentials->new(
aws_access_key_id => $assume_role_credentials->{AccessKeyId},
aws_secret_access_key => $assume_role_credentials->{SecretAccessKey},
expiration => $assume_role_credentials->{Expiration},
token => $assume_role_credentials->{SessionToken},
);
# 4. make a call to another API
my $rt53 = Amazon::API::Route53->new(
credentials => $role_credentials,
);
my $list_tags_for_resources_response = $rt53->ListTagsForResources(
{ ResourceType => 'hostedzone',
ResourceIds => \@zone_ids,
}
);
As noted above, when you use multiple instances of
L<Amazon::Credentials>, the I<same> passkey is used for encrypting
credentials. To avoid this, you can pass a custom passkey when you
instantiate the L<Amazon::Credentials> object, however, you will need
to reset that passkey when you use that object.
use Amazon::Credentials qw(create_passkey);
my %passkey = (
sso => create_passkey,
role => create_passkey,
);
my $sso_creds = sub { return $passkey{sso} };
my $role_creds = sub { return $passkey{role} };
my $sso_credentials = Amazon::Credentials->new(
sso_role_name => 'developer',
sso_account_id => '01234567890'
passkey => $sso_creds,
);
...
my $role_credentials = Amazon::Credentials->new(
aws_access_key_id => $assume_role_credentials->{AccessKeyId},
aws_secret_access_key => $assume_role_credentials->{SecretAccessKey},
token => $assume_role_credentials->{SessionToken},
expiration => $assume_role_credentials->{Expiration},
passkey => $role_creds,
);
...then later
$sso_credentials->set_passkey($sso_creds);
=item Using a Custom Cipher
As noted, the default L<Crypt::CBC> cipher is used for encrypting your
credentials, however you can pass a custom cipher supported by
L<Crypt::CBC> further obfuscating the methods used to encrypt your
credentials.
my $credentials = Amazon::Credentials(
passkey => \&fetch_passkey,
cipher => 'Blowfish'
);
=item Rotating Passkeys and Credentials
For those with the (justifiably) paranoid feeling that no matter what
you do there are those determined to crack even encrypted or obfuscated
credentials once exposed, you can periodically rotate the credentials.
If you are not using a custom passkey...
$credentials->rotate_credentials;
...or if you have a custom passkey generator your subroutine must
continue to provide the old passkey before you can reset the passkey.
use Amazon::Credentials qw( create_passkey );
my $passkey = create_passkey;
sub get_passkey {
my ($regenerate) = shift;
return $regenerate ? create_passkey : $passkey;
}
my $credentials = Amazon::Credentials->new( passkey => \&get_passkey );
$passkey = $credentials->rotate_credentials(get_passkey(1));
=item Using Custom Encryption Methods
Finally, you can also provide your own C<encrypt()> and C<decrypt()>
methods when you call the C<new()> constructor. These methods will be
passed the string to encrypt or decrypt and the passkey. Your methods
should return the decrypted or encrypted strings. Your methods can
ignore the passkey if your methods provide their own passkey or
mechanisms for encryption.
use Amazon::Credentials qw( create_passkey };
my $passkey = create_passkey();
sub my_encrypt {
my ($self, $str) = @_;
...
return $encrypted_str;
}
sub my_decrypt {
...
return $deecrypted_str;
}
my $creds = Amazon::Credentials->new( encrypt => \&my_encrypt,
decrypt => \&my_decrypt,
passkey => sub { return $passkey },
);
=back
=back
=head2 Securing Your Logs
To troubleshoot potential bugs in this module or to understand what
L<Amazon::Credentials> is doing you can pass a debug flag that will
write potentially helpful info to STDERR.
To prevent possible exposure of credentials in debug messages, the
module will not write log messages that contain your credentials even
if your debug flag is set to a true value. In order to debug output of
all content you the C<insecure> flag to any of the values shown below.
=over 5
=item insecure = false (0, '', undef)
If the debug flag is true, any message that might potentially contain
credentials is not written to STDERR. This is the default.
=item insecure = 1
Setting C<insecure> to 1 will allow more debug messages, however
credentials will be masked.
=item insecure = 2 or 'insecure'
This setting, along with setting the debug mode to a true value will
enable full debugging.
=back
=head2 Use Temporary Credentials
One additional tip to help prevent the use of your credentials even if
they have been exposed in logs or files. I<Use temporary credentials
with short expiration times whenever possible.> L<Amazon::Credentials>
provides methods to determine if your credentials have expired and
a method to refresh them when they have.
if ( $credentials->is_token_expired ) {
$credentials->refresh_token;
}
=head2 Use Granular Credentials
Consider the APIs that you are calling with these credentials. If all
you need to do is access a bucket or a key within a bucket, use
credentials that B<ONLY> allow access to that bucket. IAM permissions
can be quite specific regarding what and from where credentials can be
used to access resources.
=head2 Additonal Notes on Logging
Versions I<1.0.18> and I<1.0.19> allowed you to enable debugging by
setting the environment variable DEBUG to any true value to enable
basic debug output. Version I<1.0.18> would log information to STDERR
including payloads that might contain credentials. Version I<1.0.19>
would prevent writing any payload with credentials I<unless> the debug
mode was set to 2 or 'insecure'. Keep in mind however that you should
avoid allowing upstream programs to use environment variables to set
debugging modes that you might pass to L<Amazon::Credentials>.
Starting with version I<1.1.0> the L<Amazon::Credentials> will B<not>
use the environment variable DEBUG to enable debugging! You must
explicitly pass the debug flag in the constructor to enable
debugging. This was done to prevent potential upstream modules that
you might use who allow an environment variable to set debug mode to
also inadvertantly trigger debug mode for L<Amazon::Credentials>.
=head1 INCOMPATIBILITIES
This module has not been tested on Windows OS.
=head1 CONTRIBUTING
You can find this project on GitHub at
L<https://github.com/rlauer6/perl-Amazon-Credentials>. PRs are always
welcomed!
=head1 LICENSE AND COPYRIGHT
This module is free software. It may be used, redistributed and/or
modified under the same terms as Perl itself.
=head1 AUTHOR
Rob Lauer - <rlauer6@comcast.net>
=cut