Skip to content

Commit

Permalink
plugins: Add new credential provider for AWS credential files
Browse files Browse the repository at this point in the history
Earlier users could provide sensitive values such as AWS
secret keys using environment variables. This change adds
a new AWS credential provider which reads the credential file
to fetch credentials for a named profile. If no profile is
provided, the "default" profile is used. OPA reads the
credentials from the file on each request and uses them
for authentication.

Fixes: open-policy-agent#2786

Signed-off-by: Ashutosh Narkar <anarkar4387@gmail.com>
  • Loading branch information
ashutosh-narkar committed Nov 24, 2021
1 parent 7552a61 commit e854703
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 5 deletions.
13 changes: 13 additions & 0 deletions docs/content/configuration.md
Expand Up @@ -417,6 +417,19 @@ Please note that if you are using temporary IAM credentials (e.g. assumed IAM ro
| --- | --- | --- | --- |
| `services[_].credentials.s3_signing.environment_credentials` | `{}` | Yes | Enables AWS signing using environment variables to source the configuration and credentials |


##### Using Named Profile Credentials
If specifying `profile_credentials`, OPA will expect to find the `access key id`, `secret access key` and
`session token` from the [named profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html)
stored in the [credentials](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html) file on disk. On each
request OPA will re-read the credentials from the file and use them for authentication.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.s3_signing.profile_credentials.path` | `string` | No | The path to the shared credentials file. If empty, OPA will look for the `AWS_SHARED_CREDENTIALS_FILE` env variable. If the variable is not set, the path defaults to the current user's home directory. `~/.aws/credentials` (Linux & Mac) or `%USERPROFILE%\.aws\credentials` (Windows) |
| `services[_].credentials.s3_signing.profile_credentials.profile` | `string` | No | AWS Profile to extract credentials from the credentials file. If empty, OPA will look for the `AWS_PROFILE` env variable. If the variable is not set, the `default` profile will be used |
| `services[_].credentials.s3_signing.metadata_credentials.aws_region` | `string` | No | The AWS region to use for the AWS signing service credential method. If unset, the `AWS_REGION` environment variable must be set |

##### Using EC2 Metadata Credentials
If specifying `metadata_credentials`, OPA will use the AWS metadata services for [EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)
or [ECS](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html)
Expand Down
114 changes: 113 additions & 1 deletion plugins/rest/aws.go
Expand Up @@ -16,10 +16,14 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"

"github.com/go-ini/ini"

"github.com/open-policy-agent/opa/logging"
)

Expand All @@ -46,6 +50,13 @@ const (
awsRegionEnvVar = "AWS_REGION"
awsRoleArnEnvVar = "AWS_ROLE_ARN"
awsWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE"
awsProfileEnvVar = "AWS_PROFILE"

// ref. https://docs.aws.amazon.com/sdkref/latest/guide/settings-global.html
accessKeyGlobalSetting = "aws_access_key_id"
secretKeyGlobalSetting = "aws_secret_access_key"
securityTokenGlobalSetting = "aws_session_token"
)

// Headers that may be mutated before reaching an aws service (eg by a proxy) should be added here to omit them from
Expand Down Expand Up @@ -89,7 +100,7 @@ func (cs *awsEnvironmentCredentialService) credentials() (awsCredentials, error)
if creds.RegionName == "" {
return creds, errors.New("no " + awsRegionEnvVar + " set in environment")
}
// SessionToken is required if using temporaty ENV credentials from assumed IAM role
// SessionToken is required if using temporary ENV credentials from assumed IAM role
// Missing SessionToken results with 403 s3 error.
creds.SessionToken = os.Getenv(sessionTokenEnvVar)
if creds.SessionToken == "" {
Expand All @@ -101,6 +112,107 @@ func (cs *awsEnvironmentCredentialService) credentials() (awsCredentials, error)
return creds, nil
}

// awsProfileCredentialService represents a credential provider for AWS that extracts credentials from the AWS
// credentials file
type awsProfileCredentialService struct {

// Path to the credentials file.
//
// If empty will look for "AWS_SHARED_CREDENTIALS_FILE" env variable. If the
// env value is empty will default to current user's home directory.
// Linux/OSX: "$HOME/.aws/credentials"
// Windows: "%USERPROFILE%\.aws\credentials"
Path string `json:"path,omitempty"`

// AWS Profile to extract credentials from the credentials file. If empty
// will default to environment variable "AWS_PROFILE" or "default" if
// environment variable is also not set.
Profile string `json:"profile,omitempty"`

RegionName string `json:"aws_region"`

logger logging.Logger
}

func (cs *awsProfileCredentialService) credentials() (awsCredentials, error) {
var creds awsCredentials

filename, err := cs.path()
if err != nil {
return creds, err
}

cfg, err := ini.Load(filename)
if err != nil {
return creds, fmt.Errorf("failed to read credentials file: %v", err)
}

profile, err := cfg.GetSection(cs.profile())
if err != nil {
return creds, fmt.Errorf("failed to get profile: %v", err)
}

creds.AccessKey = profile.Key(accessKeyGlobalSetting).String()
if creds.AccessKey == "" {
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, accessKeyGlobalSetting)
}

creds.SecretKey = profile.Key(secretKeyGlobalSetting).String()
if creds.SecretKey == "" {
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, secretKeyGlobalSetting)
}

creds.SessionToken = profile.Key(securityTokenGlobalSetting).String() //default to empty string

if cs.RegionName == "" {
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
return creds, errors.New("no " + awsRegionEnvVar + " set in environment or configuration")
}
}
creds.RegionName = cs.RegionName

return creds, nil
}

func (cs *awsProfileCredentialService) path() (string, error) {
if len(cs.Path) != 0 {
return cs.Path, nil
}

if cs.Path = os.Getenv(awsCredentialsFileEnvVar); len(cs.Path) != 0 {
return cs.Path, nil
}

var homeDir string
if runtime.GOOS == "windows" {
homeDir = os.Getenv("USERPROFILE")
} else {
homeDir = os.Getenv("HOME")
}

if len(homeDir) == 0 {
return "", errors.New("user home directory not found")
}

cs.Path = filepath.Join(homeDir, ".aws", "credentials")

return cs.Path, nil
}

func (cs *awsProfileCredentialService) profile() string {
if cs.Profile != "" {
return cs.Profile
}

cs.Profile = os.Getenv(awsProfileEnvVar)

if cs.Profile == "" {
cs.Profile = "default"
}

return cs.Profile
}

// awsMetadataCredentialService represents an EC2 metadata service credential provider for AWS
type awsMetadataCredentialService struct {
RoleName string `json:"iam_role,omitempty"`
Expand Down

0 comments on commit e854703

Please sign in to comment.