Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support to IAM authentication in the datastore #4828

Merged
merged 13 commits into from
Feb 27, 2024

Conversation

amartinezfayo
Copy link
Member

@amartinezfayo amartinezfayo commented Jan 23, 2024

This PR introduces support to authenticate to the datastore using IAM database authentication for PostgreSQL and MySQL databases hosted in AWS RDS.

This is done by supporting the new aws_postgres and aws_mysql database types in the datastore configuration.

Fixes #2283.

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
// Open is the overridden method for opening a connection, using
// AWS IAM authentication
func (w *sqlDriverWrapper) Open(name string) (driver.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeOut)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to not use a derived context here and instead using a context.Background here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is that sql.Open does not allow to provide a context, so we are limited by the arguments that are there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could have injected the context to the wrapper so it could be used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @marcofranssen for you feedback.
The wrapper driver instance is created at initialization time. The wrapper struct could still have a context that could be set externally, but we would need to have a map between that context and a future call to Open (probably with the string used in the Open call as the key). All that seemed to be error-prone and adding a complexity that we may not really need. I would rather explore alternatives if we see that having a separate context causes problems.

}
password, err := token.getAWSAuthToken(ctx, config, w.tokenBuilder)
if err != nil {
return nil, fmt.Errorf("could not get authorization token: %w", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("could not get authorization token: %w", err)
return nil, fmt.Errorf("could not get authentication token: %w", err)

@@ -135,6 +135,60 @@ If you need to use custom Root CA, just specify `root_ca_path` in the plugin con
}
```

### IAM Authentication

With IAM database authentication, an authentication token is used instead of a password when connecting to the database. For that reason, a password must not be provided in the connection string.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
With IAM database authentication, an authentication token is used instead of a password when connecting to the database. For that reason, a password must not be provided in the connection string.
AWS Identity and Access Management (IAM) authentication allows for secure authentication to databases hosted on AWS. Unlike traditional methods, it uses an authentication token instead of a password. When using IAM authentication, it is required to exclude the password from the connection string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggested change. Since IAM authentication is not tied to AWS IAM only (we will be supporting IAM authentication in other services from other cloud providers) the description should not be AWS-specific.
I'm taking this but without the AWS-specific parts. Thanks for the suggestion, it's a lot more clear! than the original text


With IAM database authentication, an authentication token is used instead of a password when connecting to the database. For that reason, a password must not be provided in the connection string.

The `database_type` configuration can have additional settings when a database type supporting IAM authentication is used, and always takes the following form:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `database_type` configuration can have additional settings when a database type supporting IAM authentication is used, and always takes the following form:
The database_type configuration allows specifying the type of database with IAM support. The configuration always follows this structure:

...
}
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Note: Replace `dbtype-with-iam-support` with the specific database type that supports IAM authentication.

return nil, "", false, err
}
if c.Password != "" {
return nil, "", false, errors.New("invalid postgres config: no password should be provided when using IAM authentication")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, "", false, errors.New("invalid postgres config: no password should be provided when using IAM authentication")
return nil, "", false, errors.New("invalid postgres configuration: password should not be set when using IAM authentication.")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Note that error strings should not end with punctuation, so I'll not end it with ".".

}

type tokenGetter interface {
getAWSAuthToken(ctx context.Context, params *Config, tokenBuilder authTokenBuilder) (string, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename to getAuthToken to keep it consistent with the naming in the authTokenBuilder interface.

MySQLDriverName = "aws-rds-mysql"
PostgresDriverName = "aws-rds-postgres"
iso8601BasicFormat = "20060102T150405Z"
timeOut = time.Second * 30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
timeOut = time.Second * 30
connectionTimeout = time.Second * 30

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This timeout is not a connection timeout to the database, but a timeout building the authentication token. Maybe buildAuthTokenTimeout is a better name.

const (
MySQLDriverName = "aws-rds-mysql"
PostgresDriverName = "aws-rds-postgres"
iso8601BasicFormat = "20060102T150405Z"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this constant to the other file where it's used.

return "", fmt.Errorf("could not parse connection string: %w", err)
}
if cfg.Password != "" {
return "", errors.New("password was provided in the connection string")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return "", errors.New("password was provided in the connection string")
return "", errors.New("unexpected password in connection string for IAM authentication")

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
@evan2645 evan2645 modified the milestones: 1.9.0, 1.9.1 Feb 15, 2024
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
```hcl
DataStore "sql" {
plugin_data {
database_type "aws_postgres" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation is off


func (a *authToken) isExpired() bool {
clockSkew := time.Minute // Make sure that the authentication token is valid for one more minute.
return nowFunc().Add(clockSkew).Sub(a.expiresAt) >= 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be subtracting a minute from the current time if we want the token to live a little longer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, we should be subtracting the clock skew, not adding.


// FormatDSN returns a DSN string based on the configuration.
func (c *Config) FormatDSN() (string, error) {
dsn, err := json.Marshal(c)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this seems ok since it's wholly contained by the driver implementation. I wonder if encoded url.Values might be a little more natural choice...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep it as is unless you have a strong opinion to change to url.Values.

type sqlDriverWrapper struct {
sqlDriver driver.Driver
tokenBuilder authTokenBuilder
tokensMap tokens
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can Open() be called concurrently? Do we need to protect this with a mutex?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this when I originally thought about this. I now think that to be in the safe side we should have a mutex.

if cfg.Password != "" {
return "", errors.New("unexpected password in connection string for IAM authentication")
}
return fmt.Sprintf("%s password=%s", connString, password), nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the password value be quoted? (e.g. %q)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although we don't really expect spaces in an AWS auth token, it's a good point to consider.
Using %q wouldn't work here, as the value should be surrounded with single quotes, not double quotes.
To have an even more robust implementation, I'm thinking that I should escape the special characters ' and \ from this value.
I'll make the changes to enclose the value in single quotes and escape single quotes and backslashes. Thank you for bringing this up!

return cfg.FormatDSN(), nil
}

func init() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: init() functions should be moved to the top where we typically have other things that are initialized at package init time

return nil, d.err
}

func init() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init should ideally go towards the top of the file

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
azdagron
azdagron previously approved these changes Feb 27, 2024
Copy link
Member

@azdagron azdagron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; one non-blocking comment.

@@ -85,6 +94,9 @@ func (w *sqlDriverWrapper) Open(name string) (driver.Conn, error) {
return nil, fmt.Errorf("could not unmarshal configuration: %w", err)
}

w.tokensMapMtx.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if sqlDriver.Open can block for some time, but if so, maybe we can shorten the scope of this lock so it doesn't block other attempts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good idea to shorten the scope.

Copy link
Member

@maxlambrecht maxlambrecht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! 👍

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
azdagron
azdagron previously approved these changes Feb 27, 2024
Copy link
Member

@azdagron azdagron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @amartinezfayo !

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
@amartinezfayo amartinezfayo merged commit 49f8857 into spiffe:main Feb 27, 2024
32 checks passed
rushi47 pushed a commit to rushi47/spire that referenced this pull request Apr 11, 2024
* Introduce support to IAM authentication in the datastore

Signed-off-by: Agustín Martínez Fayó <amartinezfayo@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow for credential introspection in AWS for Database backends
5 participants