mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
The use of a re-badged buildbox image (rebadging quay.io/gravitational/teleport-buildbox to us-docker.pkg.dev/ci-account/teleport/buildbox-root) is holdover from when CI builds required separate "root user" and "non-root user" images to execute tests under different contexts. This requirement has since been lifted by using a Go-based build script in CI, and the having to manually invoke a process to re-badge the buildboxes is, tedious, confusing and causing stoppages. This patch restores the use of buildboxes from quay.io in CI.
250 lines
7.9 KiB
Go
250 lines
7.9 KiB
Go
/*
|
|
Copyright 2021 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/textproto"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/arn"
|
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
|
"github.com/gravitational/trace"
|
|
)
|
|
|
|
const (
|
|
// AmazonSigV4AuthorizationPrefix is AWS Authorization prefix indicating that the request
|
|
// was signed by AWS Signature Version 4.
|
|
// https://github.com/aws/aws-sdk-go/blob/main/aws/signer/v4/v4.go#L83
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
|
|
AmazonSigV4AuthorizationPrefix = "AWS4-HMAC-SHA256"
|
|
|
|
// AmzDateTimeFormat is time format used in X-Amz-Date header.
|
|
// https://github.com/aws/aws-sdk-go/blob/main/aws/signer/v4/v4.go#L84
|
|
AmzDateTimeFormat = "20060102T150405Z"
|
|
|
|
// AmzDateHeader is header name containing timestamp when signature was generated.
|
|
// https://docs.aws.amazon.com/general/latest/gr/sigv4-date-handling.html
|
|
AmzDateHeader = "X-Amz-Date"
|
|
|
|
AuthorizationHeader = "Authorization"
|
|
credentialAuthHeaderElem = "Credential"
|
|
signedHeaderAuthHeaderElem = "SignedHeaders"
|
|
signatureAuthHeaderElem = "Signature"
|
|
)
|
|
|
|
// SigV4 contains parsed content of the AWS Authorization header.
|
|
type SigV4 struct {
|
|
// KeyIS is an AWS access-key-id
|
|
KeyID string
|
|
// Date value is specified using YYYYMMDD format.
|
|
Date string
|
|
// Region is an AWS Region.
|
|
Region string
|
|
// Service is an AWS Service.
|
|
Service string
|
|
// SignedHeaders is a list of request headers that you used to compute Signature.
|
|
SignedHeaders []string
|
|
// Signature is the 256-bit Signature of the request.
|
|
Signature string
|
|
}
|
|
|
|
// ParseSigV4 AWS SigV4 credentials string sections.
|
|
// AWS SigV4 header example:
|
|
// Authorization: AWS4-HMAC-SHA256
|
|
// Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
|
|
// SignedHeaders=host;range;x-amz-date,
|
|
// Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
|
|
func ParseSigV4(header string) (*SigV4, error) {
|
|
if header == "" {
|
|
return nil, trace.BadParameter("empty AWS SigV4 header")
|
|
}
|
|
sectionParts := strings.Split(header, " ")
|
|
|
|
m := make(map[string]string)
|
|
for _, v := range sectionParts {
|
|
kv := strings.Split(v, "=")
|
|
if len(kv) != 2 {
|
|
continue
|
|
}
|
|
m[kv[0]] = strings.TrimSuffix(kv[1], ",")
|
|
}
|
|
|
|
authParts := strings.Split(m[credentialAuthHeaderElem], "/")
|
|
if len(authParts) != 5 {
|
|
return nil, trace.BadParameter("invalid size of %q section", credentialAuthHeaderElem)
|
|
}
|
|
|
|
signature := m[signatureAuthHeaderElem]
|
|
if signature == "" {
|
|
return nil, trace.BadParameter("invalid signature")
|
|
}
|
|
var signedHeaders []string
|
|
if v := m[signedHeaderAuthHeaderElem]; v != "" {
|
|
signedHeaders = strings.Split(v, ";")
|
|
}
|
|
|
|
return &SigV4{
|
|
KeyID: authParts[0],
|
|
Date: authParts[1],
|
|
Region: authParts[2],
|
|
Service: authParts[3],
|
|
Signature: signature,
|
|
// Split semicolon-separated list of signed headers string.
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
|
|
// https://github.com/aws/aws-sdk-go/blob/main/aws/signer/v4/v4.go#L631
|
|
SignedHeaders: signedHeaders,
|
|
}, nil
|
|
}
|
|
|
|
// IsSignedByAWSSigV4 checks is the request was signed by AWS Signature Version 4 algorithm.
|
|
// https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html
|
|
func IsSignedByAWSSigV4(r *http.Request) bool {
|
|
return strings.HasPrefix(r.Header.Get(AuthorizationHeader), AmazonSigV4AuthorizationPrefix)
|
|
}
|
|
|
|
// GetAndReplaceReqBody returns the request and replace the drained body reader with io.NopCloser
|
|
// allowing for further body processing by http transport.
|
|
func GetAndReplaceReqBody(req *http.Request) ([]byte, error) {
|
|
if req.Body == nil || req.Body == http.NoBody {
|
|
return []byte{}, nil
|
|
}
|
|
// req.Body is closed during drainBody call.
|
|
payload, err := drainBody(req.Body)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Replace the drained body with io.NopCloser reader allowing for further request processing by HTTP transport.
|
|
req.Body = io.NopCloser(bytes.NewReader(payload))
|
|
return payload, nil
|
|
}
|
|
|
|
// drainBody drains the body, close the reader and returns the read bytes.
|
|
func drainBody(b io.ReadCloser) ([]byte, error) {
|
|
payload, err := io.ReadAll(b)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
if err = b.Close(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
// VerifyAWSSignature verifies the request signature ensuring that the request originates from tsh aws command execution
|
|
// AWS CLI signs the request with random generated credentials that are passed to LocalProxy by
|
|
// the AWSCredentials LocalProxyConfig configuration.
|
|
func VerifyAWSSignature(req *http.Request, credentials *credentials.Credentials) error {
|
|
sigV4, err := ParseSigV4(req.Header.Get("Authorization"))
|
|
if err != nil {
|
|
return trace.BadParameter(err.Error())
|
|
}
|
|
// Read the request body and replace the body ready with a new reader that will allow reading the body again
|
|
// by HTTP Transport.
|
|
payload, err := GetAndReplaceReqBody(req)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
reqCopy := req.Clone(context.Background())
|
|
|
|
// Remove all the headers that are not present in awsCred.SignedHeaders.
|
|
filterHeaders(reqCopy, sigV4.SignedHeaders)
|
|
|
|
// Get the date that was used to create the signature of the original request
|
|
// originated from AWS CLI and reuse it as a timestamp during request signing call.
|
|
t, err := time.Parse(AmzDateTimeFormat, reqCopy.Header.Get(AmzDateHeader))
|
|
if err != nil {
|
|
return trace.BadParameter(err.Error())
|
|
}
|
|
|
|
signer := v4.NewSigner(credentials)
|
|
_, err = signer.Sign(reqCopy, bytes.NewReader(payload), sigV4.Service, sigV4.Region, t)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
localSigV4, err := ParseSigV4(reqCopy.Header.Get("Authorization"))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Compare the origin request AWS SigV4 signature with the signature calculated in LocalProxy based on
|
|
// AWSCredentials taken from LocalProxyConfig.
|
|
if sigV4.Signature != localSigV4.Signature {
|
|
return trace.AccessDenied("signature verification failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// filterHeaders removes request headers that are not in the headers list.
|
|
func filterHeaders(r *http.Request, headers []string) {
|
|
out := make(http.Header)
|
|
for _, v := range headers {
|
|
ck := textproto.CanonicalMIMEHeaderKey(v)
|
|
val, ok := r.Header[ck]
|
|
if ok {
|
|
out[ck] = val
|
|
}
|
|
}
|
|
r.Header = out
|
|
}
|
|
|
|
// FilterAWSRoles returns role ARNs from the provided list that belong to the
|
|
// specified AWS account ID.
|
|
//
|
|
// If AWS account ID is empty, all roles are returned.
|
|
func FilterAWSRoles(arns []string, accountID string) (result []Role) {
|
|
for _, roleARN := range arns {
|
|
parsed, err := arn.Parse(roleARN)
|
|
if err != nil || (accountID != "" && parsed.AccountID != accountID) {
|
|
continue
|
|
}
|
|
|
|
// In AWS convention, the display of the role is the last
|
|
// /-delineated substring.
|
|
//
|
|
// Example ARNs:
|
|
// arn:aws:iam::1234567890:role/EC2FullAccess (display: EC2FullAccess)
|
|
// arn:aws:iam::1234567890:role/path/to/customrole (display: customrole)
|
|
parts := strings.Split(parsed.Resource, "/")
|
|
numParts := len(parts)
|
|
if numParts < 2 || parts[0] != "role" {
|
|
continue
|
|
}
|
|
result = append(result, Role{
|
|
Display: parts[numParts-1],
|
|
ARN: roleARN,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Role describes an AWS IAM role for AWS console access.
|
|
type Role struct {
|
|
// Display is the role display name.
|
|
Display string `json:"display"`
|
|
// ARN is the full role ARN.
|
|
ARN string `json:"arn"`
|
|
}
|