1
0
Fork 0
mirror of https://github.com/fog/fog-aws.git synced 2022-11-09 13:50:52 -05:00

Prior to this change the behavior of the library has been

to refresh the credential from IAM only after its in the last
15 seconds of its life according to its expired_at field

This poses a problem when using the library to generate short
lived pre-signed URLs in AWS S3 where the issued URLs
with temporary tokens expire the same time as the IAM credential
that generated them:
https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html

For example, if the credentials is in its last 16 seconds of life
and a token has been generated for pre-signed URLs with a life
request of 10 minutes, the URL ends up living only 16 seconds.

By allowing the caller to configure the refresh threshold a
larger buffer of time can be specified when using IAM credentials
allowing for the generated URLs to life a more definitive time.

This change adds a configuration option that can be passed by
callers to expand (or narrow) the threshold of time used by the
refresh credentials mechanism to decide if a new token needs
to be fetched.

Configuration option is called
'aws_credentials_refresh_threshold_seconds'

Following the same example, setting this new option to 10 minutes
can help guarantee that pre-signed URLs generated for 10 minutes
will always last that long.
This commit is contained in:
Harsh Chouraria 2022-03-07 18:20:33 +05:30
parent 5b25c70ba1
commit 92827e574c
4 changed files with 100 additions and 11 deletions

View file

@ -105,6 +105,48 @@ directory.files
directory.files.new(key: 'user/1/Gemfile').url(Time.now + 60) directory.files.new(key: 'user/1/Gemfile').url(Time.now + 60)
``` ```
##### Controlling credential refresh time with IAM authentication
When using IAM authentication with
[temporary security credentials](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html),
generated S3 pre-signed URLs
[only last as long as the temporary credential](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html).
Generating the URLs in the following manner will return a URL
that will not last as long as its requested expiration time if
the remainder of the authentication token lifetime was shorter.
```ruby
s3 = Fog::Storage.new(provider: 'AWS', use_iam_auth: true)
directory = s3.directories.get('gaudi-portal-dev', prefix: 'user/1/')
directory.files.new(key: 'user/1/Gemfile').url(Time.now + 60)
```
By default the temporary credentials in use are refreshed only within the last
15 seconds of its expiration time. The URL requested with 60 seconds lifetime
using the above example will only remain valid for 15 seconds in the worst case.
The problem can be avoided by refreshing the token early and often,
by setting configuration `aws_credentials_refresh_threshold_seconds` (default: 15)
which controls the time when the refresh must occur. It is expressed in seconds
before the temporary credential's expiration time.
The following example can ensure pre-signed URLs last as long as 60 seconds
by automatically refreshing the credentials when its remainder lifetime
is lower than 60 seconds:
```ruby
s3 = Fog::Storage.new(
provider: 'AWS',
use_iam_auth: true,
aws_credentials_refresh_threshold_seconds: 60
)
directory = s3.directories.get('gaudi-portal-dev', prefix: 'user/1/')
directory.files.new(key: 'user/1/Gemfile').url(Time.now + 60)
```
#### Copying a file #### Copying a file
```ruby ```ruby

View file

@ -116,10 +116,17 @@ module Fog
private private
# When defined, 'aws_credentials_refresh_threshold_seconds' controls
# when the credential needs to be refreshed, expressed in seconds before
# the current credential's expiration time
def credentials_refresh_threshold
@aws_credentials_refresh_threshold_seconds || 15
end
def credentials_expired? def credentials_expired?
@use_iam_profile && @use_iam_profile &&
(!@aws_credentials_expire_at || (!@aws_credentials_expire_at ||
(@aws_credentials_expire_at && Fog::Time.now > @aws_credentials_expire_at - 15)) #new credentials become available from around 5 minutes before expiration time (@aws_credentials_expire_at && Fog::Time.now > @aws_credentials_expire_at - credentials_refresh_threshold)) #new credentials become available from around 5 minutes before expiration time
end end
def refresh_credentials def refresh_credentials

View file

@ -43,7 +43,7 @@ module Fog
] ]
requires :aws_access_key_id, :aws_secret_access_key requires :aws_access_key_id, :aws_secret_access_key
recognizes :endpoint, :region, :host, :port, :scheme, :persistent, :use_iam_profile, :aws_session_token, :aws_credentials_expire_at, :path_style, :acceleration, :instrumentor, :instrumentor_name, :aws_signature_version, :enable_signature_v4_streaming, :virtual_host, :cname recognizes :endpoint, :region, :host, :port, :scheme, :persistent, :use_iam_profile, :aws_session_token, :aws_credentials_expire_at, :path_style, :acceleration, :instrumentor, :instrumentor_name, :aws_signature_version, :enable_signature_v4_streaming, :virtual_host, :cname, :aws_credentials_refresh_threshold_seconds
secrets :aws_secret_access_key, :hmac secrets :aws_secret_access_key, :hmac
@ -466,6 +466,8 @@ module Fog
end end
def setup_credentials(options) def setup_credentials(options)
@aws_credentials_refresh_threshold_seconds = options[:aws_credentials_refresh_threshold_seconds]
@aws_access_key_id = options[:aws_access_key_id] @aws_access_key_id = options[:aws_access_key_id]
@aws_secret_access_key = options[:aws_secret_access_key] @aws_secret_access_key = options[:aws_secret_access_key]
@aws_session_token = options[:aws_session_token] @aws_session_token = options[:aws_session_token]
@ -540,6 +542,8 @@ module Fog
def setup_credentials(options) def setup_credentials(options)
@aws_credentials_refresh_threshold_seconds = options[:aws_credentials_refresh_threshold_seconds]
@aws_access_key_id = options[:aws_access_key_id] @aws_access_key_id = options[:aws_access_key_id]
@aws_secret_access_key = options[:aws_secret_access_key] @aws_secret_access_key = options[:aws_secret_access_key]
@aws_session_token = options[:aws_session_token] @aws_session_token = options[:aws_session_token]

View file

@ -101,25 +101,61 @@ Shindo.tests('AWS | credentials', ['aws']) do
ENV['AWS_WEB_IDENTITY_TOKEN_FILE'] = nil ENV['AWS_WEB_IDENTITY_TOKEN_FILE'] = nil
compute = Fog::AWS::Compute.new(use_iam_profile: true) storage = Fog::Storage.new(
:provider => 'AWS',
:region => 'us-east-1',
:use_iam_profile => true,
:aws_credentials_refresh_threshold_seconds => 30)
tests('#refresh_credentials_if_expired') do tests('#credentials_refresh_threshold') do
returns(nil) { compute.refresh_credentials_if_expired } returns(30) { storage.send(:credentials_refresh_threshold) }
end end
credentials['AccessKeyId'] = 'newkey' Fog::Time.now = storage.instance_variable_get(:@aws_credentials_expire_at) - 31
credentials['SecretAccessKey'] = 'newsecret' tests('#refresh_credentials_if_expired before credentials have expired and before refresh threshold') do
returns(nil) { storage.refresh_credentials_if_expired }
returns('dummykey') { storage.instance_variable_get(:@aws_access_key_id) }
returns('dummysecret') { storage.instance_variable_get(:@aws_secret_access_key) }
returns(expires_at) { storage.instance_variable_get(:@aws_credentials_expire_at) }
end
Fog::Time.now = Time.now
credentials['AccessKeyId'] = 'newkey-1'
credentials['SecretAccessKey'] = 'newsecret-1'
credentials['Expiration'] = (expires_at + 10).xmlschema credentials['Expiration'] = (expires_at + 10).xmlschema
Excon.stub({ method: :get, path: '/latest/meta-data/iam/security-credentials/arole' }, { status: 200, body: Fog::JSON.encode(credentials) }) Excon.stub({ method: :get, path: '/latest/meta-data/iam/security-credentials/arole' }, { status: 200, body: Fog::JSON.encode(credentials) })
Fog::Time.now = expires_at + 1 Fog::Time.now = storage.instance_variable_get(:@aws_credentials_expire_at) - 29
tests('#refresh_credentials_if_expired') do tests('#refresh_credentials_if_expired after refresh threshold is crossed but before expiration') do
returns(true) { compute.refresh_credentials_if_expired } returns(true) { storage.refresh_credentials_if_expired }
returns('newkey') { compute.instance_variable_get(:@aws_access_key_id) } returns('newkey-1') { storage.instance_variable_get(:@aws_access_key_id) }
returns('newsecret-1') { storage.instance_variable_get(:@aws_secret_access_key) }
returns(expires_at + 10) { storage.instance_variable_get(:@aws_credentials_expire_at) }
end end
Fog::Time.now = Time.now Fog::Time.now = Time.now
credentials['AccessKeyId'] = 'newkey-2'
credentials['SecretAccessKey'] = 'newsecret-2'
credentials['Expiration'] = (expires_at + 20).xmlschema
Excon.stub({ method: :get, path: '/latest/meta-data/iam/security-credentials/arole' }, { status: 200, body: Fog::JSON.encode(credentials) })
Fog::Time.now = storage.instance_variable_get(:@aws_credentials_expire_at) + 1
tests('#refresh_credentials_if_expired after credentials have expired') do
returns(true) { storage.refresh_credentials_if_expired }
returns('newkey-2') { storage.instance_variable_get(:@aws_access_key_id) }
returns('newsecret-2') { storage.instance_variable_get(:@aws_secret_access_key) }
returns(expires_at + 20) { storage.instance_variable_get(:@aws_credentials_expire_at) }
end
Fog::Time.now = Time.now
compute = Fog::AWS::Compute.new(use_iam_profile: true)
tests('#credentials_refresh_threshold when "aws_credentials_refresh_threshold_seconds" is unspecified') do
returns(15) { compute.send(:credentials_refresh_threshold) }
end
default_credentials = Fog::AWS::Compute.fetch_credentials({}) default_credentials = Fog::AWS::Compute.fetch_credentials({})
tests('#fetch_credentials when the url 404s') do tests('#fetch_credentials when the url 404s') do
Excon.stub({ method: :put, path: '/latest/api/token' }, { status: 404, body: 'not found' }) Excon.stub({ method: :put, path: '/latest/api/token' }, { status: 404, body: 'not found' })