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

plugin/rest: Adds AWS Web Identity support #2725

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/content/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ OPA will authenticate with an [AWS4 HMAC](https://docs.aws.amazon.com/AmazonS3/l
necessary credentials are available; exactly one must be specified to use the AWS signature
authentication method.

##### Using Static Environment Credentials
If specifying `environment_credentials`, OPA will expect to find environment variables
for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`, in accordance with the
convention used by the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html).
Expand All @@ -299,6 +300,7 @@ 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 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)
to obtain the necessary credentials when running within a supported virtual machine/container.
Expand All @@ -315,9 +317,17 @@ there is no route to the EC2 metadata service from inside the container or if th

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.s3_signing.metadata_credentials.aws_region` | `string` | Yes | The AWS region to use for the AWS signing service credential method |
| `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 |
| `services[_].credentials.s3_signing.metadata_credentials.iam_role` | `string` | No | The IAM role to use for the AWS signing service credential method |

##### Using EKS IAM Roles for Service Account (Web Identity) Credentials
If specifying `web_identity_credentials`, OPA will expect to find environment variables for `AWS_ROLE_ARN` and `AWS_WEB_IDENTITY_TOKEN_FILE`, in accordance with the convention used by the [AWS EKS IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html#pod-configuration).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.s3_signing.web_identity_credentials.aws_region` | `string` | Yes | The AWS region to use for the sts regional endpoint. Uses the global endpoint by default |
| `services[_].credentials.s3_signing.web_identity_credentials.session_name` | `string` | No | The session name used to identify the assumed role session. Default: `open-policy-agent` |

> Services can be defined as an array or object. When defined as an object, the
> object keys override the `services[_].name` fields.
> For example:
Expand Down
143 changes: 137 additions & 6 deletions plugins/rest/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"sort"
"strings"
Expand All @@ -31,12 +33,18 @@ const (
ecsDefaultCredServicePath = "http://169.254.170.2"
ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"

// ref. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
stsDefaultPath = "https://sts.amazonaws.com"
stsRegionPath = "https://sts.%s.amazonaws.com"

// ref. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
accessKeyEnvVar = "AWS_ACCESS_KEY_ID"
secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
securityTokenEnvVar = "AWS_SECURITY_TOKEN"
sessionTokenEnvVar = "AWS_SESSION_TOKEN"
awsRegionEnvVar = "AWS_REGION"
accessKeyEnvVar = "AWS_ACCESS_KEY_ID"
secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
securityTokenEnvVar = "AWS_SECURITY_TOKEN"
sessionTokenEnvVar = "AWS_SESSION_TOKEN"
awsRegionEnvVar = "AWS_REGION"
awsRoleArnEnvVar = "AWS_ROLE_ARN"
awsWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
)

// awsCredentials represents the credentials obtained from an AWS credential provider
Expand All @@ -52,7 +60,7 @@ type awsCredentialService interface {
credentials() (awsCredentials, error)
}

// awsEnvironmentCredentialService represents an environment-variable credential provider for AWS
// awsEnvironmentCredentialService represents an static environment-variable credential provider for AWS
type awsEnvironmentCredentialService struct{}

func (cs *awsEnvironmentCredentialService) credentials() (awsCredentials, error) {
Expand Down Expand Up @@ -209,6 +217,129 @@ func (cs *awsMetadataCredentialService) credentials() (awsCredentials, error) {
return cs.creds, nil
}

// awsWebIdentityCredentialService represents an STS WebIdentity credential services
type awsWebIdentityCredentialService struct {
RoleArn string
WebIdentityTokenFile string
RegionName string `json:"aws_region"`
SessionName string `json:"session_name"`
stsURL string
creds awsCredentials
expiration time.Time
}

func (cs *awsWebIdentityCredentialService) populateFromEnv() error {
cs.RoleArn = os.Getenv(awsRoleArnEnvVar)
if cs.RoleArn == "" {
return errors.New("no " + awsRoleArnEnvVar + " set in environment")
}
cs.WebIdentityTokenFile = os.Getenv(awsWebIdentityTokenFileEnvVar)
if cs.WebIdentityTokenFile == "" {
return errors.New("no " + awsWebIdentityTokenFileEnvVar + " set in environment")
}

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

func (cs *awsWebIdentityCredentialService) stsPath() string {
var stsPath string
switch {
case cs.stsURL != "":
stsPath = cs.stsURL
case cs.RegionName != "":
stsPath = fmt.Sprintf(stsRegionPath, strings.ToLower(cs.RegionName))
default:
stsPath = stsDefaultPath
}
return stsPath
}

func (cs *awsWebIdentityCredentialService) refreshFromService() error {
// define the expected JSON payload from the EC2 credential service
// ref. https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
type responsePayload struct {
Result struct {
Credentials struct {
SessionToken string
SecretAccessKey string
Expiration time.Time
AccessKeyID string `xml:"AccessKeyId"`
}
} `xml:"AssumeRoleWithWebIdentityResult"`
}

// short circuit if a reasonable amount of time until credential expiration remains
if time.Now().Add(time.Minute * 5).Before(cs.expiration) {
logrus.Debug("Credentials previously obtained from sts service still valid.")
return nil
}

logrus.Debugf("Obtaining credentials from sts for role %s.", cs.RoleArn)

var sessionName string
if cs.SessionName == "" {
sessionName = "open-policy-agent"
} else {
sessionName = cs.SessionName
}

tokenData, err := ioutil.ReadFile(cs.WebIdentityTokenFile)
if err != nil {
return errors.New("unable to read web token for sts HTTP request: " + err.Error())
}

token := string(tokenData)

queryVals := url.Values{
"Action": []string{"AssumeRoleWithWebIdentity"},
"RoleSessionName": []string{sessionName},
"RoleArn": []string{cs.RoleArn},
"WebIdentityToken": []string{token},
"Version": []string{"2011-06-15"},
}
stsRequestURL, _ := url.Parse(cs.stsPath())
stsRequestURL.RawQuery = queryVals.Encode()

// construct an HTTP client with a reasonably short timeout
client := &http.Client{Timeout: time.Second * 10}
req, err := http.NewRequest(http.MethodGet, stsRequestURL.String(), nil)
if err != nil {
return errors.New("unable to construct STS HTTP request: " + err.Error())
}

body, err := doMetaDataRequestWithClient(req, client, "STS")
if err != nil {
return err
}

var payload responsePayload
err = xml.Unmarshal(body, &payload)
if err != nil {
return errors.New("failed to parse credential response from STS service: " + err.Error())
}

cs.expiration = payload.Result.Credentials.Expiration
cs.creds.AccessKey = payload.Result.Credentials.AccessKeyID
cs.creds.SecretKey = payload.Result.Credentials.SecretAccessKey
cs.creds.SessionToken = payload.Result.Credentials.SessionToken
cs.creds.RegionName = cs.RegionName

return nil
}

func (cs *awsWebIdentityCredentialService) credentials() (awsCredentials, error) {
err := cs.refreshFromService()
if err != nil {
return cs.creds, err
}
return cs.creds, nil
}

func isECS() bool {
// the special relative path URI is set by the container agent in the ECS environment only
_, isECS := os.LookupEnv(ecsRelativePathEnvVar)
Expand Down
Loading