Skip to content

Commit 448ba39

Browse files
author
Tyler Reid
authored
Add sigv4 module (#310)
* Add sigv4 config and throw error if a user doesn't supply username and secret key Signed-off-by: Tyler Reid <[email protected]>
1 parent 8281fb2 commit 448ba39

11 files changed

+868
-0
lines changed

.circleci/config.yml

+9
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,20 @@ jobs:
3030
condition: << parameters.run_style >>
3131
steps:
3232
- run: make style
33+
- when:
34+
condition: << parameters.run_style >>
35+
steps:
36+
- run:
37+
command: make style
38+
working_directory: /go/src/github.com/prometheus/common/sigv4
3339
- when:
3440
condition: << parameters.use_gomod_cache >>
3541
steps:
3642
- go/save-cache:
3743
key: v1-go<< parameters.go_version >>
44+
- run:
45+
command: make test
46+
working_directory: /go/src/github.com/prometheus/common/sigv4
3847
- store_test_results:
3948
path: test-results
4049

sigv4/Makefile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2018 The Prometheus Authors
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
include ../Makefile.common
15+
16+
.PHONY: test
17+
@echo ">> Running sigv4 tests"
18+
test:: deps check_license unused common-test
19+
20+
ifeq (,$(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(7|8|9|10)\.'))
21+
test:: lint
22+
endif

sigv4/go.mod

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/prometheus/common/sigv4
2+
3+
go 1.15
4+
5+
require (
6+
github.com/aws/aws-sdk-go v1.38.35
7+
github.com/prometheus/client_golang v1.11.0
8+
github.com/prometheus/common v0.29.0
9+
github.com/stretchr/testify v1.7.0
10+
gopkg.in/yaml.v2 v2.4.0
11+
)

sigv4/go.sum

+479
Large diffs are not rendered by default.

sigv4/sigv4.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package sigv4
15+
16+
import (
17+
"bytes"
18+
"fmt"
19+
"io"
20+
"io/ioutil"
21+
"net/http"
22+
"net/textproto"
23+
"sync"
24+
"time"
25+
26+
"github.com/aws/aws-sdk-go/aws"
27+
"github.com/aws/aws-sdk-go/aws/credentials"
28+
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
29+
"github.com/aws/aws-sdk-go/aws/session"
30+
signer "github.com/aws/aws-sdk-go/aws/signer/v4"
31+
)
32+
33+
var sigv4HeaderDenylist = []string{
34+
"uber-trace-id",
35+
}
36+
37+
type sigV4RoundTripper struct {
38+
region string
39+
next http.RoundTripper
40+
pool sync.Pool
41+
42+
signer *signer.Signer
43+
}
44+
45+
// NewSigV4RoundTripper returns a new http.RoundTripper that will sign requests
46+
// using Amazon's Signature Verification V4 signing procedure. The request will
47+
// then be handed off to the next RoundTripper provided by next. If next is nil,
48+
// http.DefaultTransport will be used.
49+
//
50+
// Credentials for signing are retrieved using the the default AWS credential
51+
// chain. If credentials cannot be found, an error will be returned.
52+
func NewSigV4RoundTripper(cfg *SigV4Config, next http.RoundTripper) (http.RoundTripper, error) {
53+
if next == nil {
54+
next = http.DefaultTransport
55+
}
56+
57+
creds := credentials.NewStaticCredentials(cfg.AccessKey, string(cfg.SecretKey), "")
58+
if cfg.AccessKey == "" && cfg.SecretKey == "" {
59+
creds = nil
60+
}
61+
62+
sess, err := session.NewSessionWithOptions(session.Options{
63+
Config: aws.Config{
64+
Region: aws.String(cfg.Region),
65+
Credentials: creds,
66+
},
67+
Profile: cfg.Profile,
68+
})
69+
if err != nil {
70+
return nil, fmt.Errorf("could not create new AWS session: %w", err)
71+
}
72+
if _, err := sess.Config.Credentials.Get(); err != nil {
73+
return nil, fmt.Errorf("could not get SigV4 credentials: %w", err)
74+
}
75+
if aws.StringValue(sess.Config.Region) == "" {
76+
return nil, fmt.Errorf("region not configured in sigv4 or in default credentials chain")
77+
}
78+
79+
signerCreds := sess.Config.Credentials
80+
if cfg.RoleARN != "" {
81+
signerCreds = stscreds.NewCredentials(sess, cfg.RoleARN)
82+
}
83+
84+
rt := &sigV4RoundTripper{
85+
region: cfg.Region,
86+
next: next,
87+
signer: signer.NewSigner(signerCreds),
88+
}
89+
rt.pool.New = rt.newBuf
90+
return rt, nil
91+
}
92+
93+
func (rt *sigV4RoundTripper) newBuf() interface{} {
94+
return bytes.NewBuffer(make([]byte, 0, 1024))
95+
}
96+
97+
func (rt *sigV4RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
98+
// rt.signer.Sign needs a seekable body, so we replace the body with a
99+
// buffered reader filled with the contents of original body.
100+
buf := rt.pool.Get().(*bytes.Buffer)
101+
defer func() {
102+
buf.Reset()
103+
rt.pool.Put(buf)
104+
}()
105+
if _, err := io.Copy(buf, req.Body); err != nil {
106+
return nil, err
107+
}
108+
// Close the original body since we don't need it anymore.
109+
_ = req.Body.Close()
110+
111+
// Ensure our seeker is back at the start of the buffer once we return.
112+
var seeker io.ReadSeeker = bytes.NewReader(buf.Bytes())
113+
defer func() {
114+
_, _ = seeker.Seek(0, io.SeekStart)
115+
}()
116+
req.Body = ioutil.NopCloser(seeker)
117+
118+
// Clone the request and trim out headers that we don't want to sign.
119+
signReq := req.Clone(req.Context())
120+
for _, header := range sigv4HeaderDenylist {
121+
signReq.Header.Del(header)
122+
}
123+
124+
headers, err := rt.signer.Sign(signReq, seeker, "aps", rt.region, time.Now().UTC())
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to sign request: %w", err)
127+
}
128+
129+
// Copy over signed headers. Authorization header is not returned by
130+
// rt.signer.Sign and needs to be copied separately.
131+
for k, v := range headers {
132+
req.Header[textproto.CanonicalMIMEHeaderKey(k)] = v
133+
}
134+
req.Header.Set("Authorization", signReq.Header.Get("Authorization"))
135+
136+
return rt.next.RoundTrip(req)
137+
}

sigv4/sigv4_config.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package sigv4
15+
16+
import (
17+
"fmt"
18+
19+
"github.com/prometheus/common/config"
20+
)
21+
22+
// SigV4Config is the configuration for signing remote write requests with
23+
// AWS's SigV4 verification process. Empty values will be retrieved using the
24+
// AWS default credentials chain.
25+
type SigV4Config struct {
26+
Region string `yaml:"region,omitempty"`
27+
AccessKey string `yaml:"access_key,omitempty"`
28+
SecretKey config.Secret `yaml:"secret_key,omitempty"`
29+
Profile string `yaml:"profile,omitempty"`
30+
RoleARN string `yaml:"role_arn,omitempty"`
31+
}
32+
33+
func (c *SigV4Config) Validate() error {
34+
if (c.AccessKey == "") != (c.SecretKey == "") {
35+
return fmt.Errorf("must provide a AWS SigV4 Access key and Secret Key if credentials are specified in the SigV4 config")
36+
}
37+
return nil
38+
}
39+
40+
func (c *SigV4Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
41+
type plain SigV4Config
42+
*c = SigV4Config{}
43+
if err := unmarshal((*plain)(c)); err != nil {
44+
return err
45+
}
46+
return c.Validate()
47+
}

sigv4/sigv4_config_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package sigv4
15+
16+
import (
17+
"io/ioutil"
18+
"strings"
19+
"testing"
20+
21+
"gopkg.in/yaml.v2"
22+
)
23+
24+
func loadSigv4Config(filename string) (*SigV4Config, error) {
25+
content, err := ioutil.ReadFile(filename)
26+
if err != nil {
27+
return nil, err
28+
}
29+
cfg := SigV4Config{}
30+
if err = yaml.UnmarshalStrict(content, &cfg); err != nil {
31+
return nil, err
32+
}
33+
return &cfg, nil
34+
}
35+
36+
func testGoodConfig(t *testing.T, filename string) {
37+
_, err := loadSigv4Config(filename)
38+
if err != nil {
39+
t.Fatalf("Unexpected error parsing %s: %s", filename, err)
40+
}
41+
}
42+
43+
func TestGoodSigV4Configs(t *testing.T) {
44+
filesToTest := []string{"testdata/sigv4_good.yaml", "testdata/sigv4_good.yaml"}
45+
for _, filename := range filesToTest {
46+
testGoodConfig(t, filename)
47+
}
48+
}
49+
50+
func TestBadSigV4Config(t *testing.T) {
51+
filename := "testdata/sigv4_bad.yaml"
52+
_, err := loadSigv4Config(filename)
53+
if err == nil {
54+
t.Fatalf("Did not receive expected error unmarshaling bad sigv4 config")
55+
}
56+
if !strings.Contains(err.Error(), "must provide a AWS SigV4 Access key and Secret Key") {
57+
t.Errorf("Received unexpected error from unmarshal of %s: %s", filename, err.Error())
58+
}
59+
}

sigv4/sigv4_test.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package sigv4
15+
16+
import (
17+
"net/http"
18+
"os"
19+
"strings"
20+
"testing"
21+
22+
"github.com/aws/aws-sdk-go/aws"
23+
"github.com/aws/aws-sdk-go/aws/credentials"
24+
"github.com/aws/aws-sdk-go/aws/session"
25+
signer "github.com/aws/aws-sdk-go/aws/signer/v4"
26+
"github.com/prometheus/client_golang/prometheus/promhttp"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
func TestSigV4_Inferred_Region(t *testing.T) {
31+
os.Setenv("AWS_ACCESS_KEY_ID", "secret")
32+
os.Setenv("AWS_SECRET_ACCESS_KEY", "token")
33+
os.Setenv("AWS_REGION", "us-west-2")
34+
35+
sess, err := session.NewSession(&aws.Config{
36+
// Setting to an empty string to demostrate the default value from the yaml
37+
// won't override the environment's region.
38+
Region: aws.String(""),
39+
})
40+
require.NoError(t, err)
41+
_, err = sess.Config.Credentials.Get()
42+
require.NoError(t, err)
43+
44+
require.NotNil(t, sess.Config.Region)
45+
require.Equal(t, "us-west-2", *sess.Config.Region)
46+
}
47+
48+
func TestSigV4RoundTripper(t *testing.T) {
49+
var gotReq *http.Request
50+
51+
rt := &sigV4RoundTripper{
52+
region: "us-east-2",
53+
next: promhttp.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
54+
gotReq = req
55+
return &http.Response{StatusCode: http.StatusOK}, nil
56+
}),
57+
signer: signer.NewSigner(credentials.NewStaticCredentials(
58+
"test-id",
59+
"secret",
60+
"token",
61+
)),
62+
}
63+
rt.pool.New = rt.newBuf
64+
65+
cli := &http.Client{Transport: rt}
66+
67+
req, err := http.NewRequest(http.MethodPost, "google.com", strings.NewReader("Hello, world!"))
68+
require.NoError(t, err)
69+
70+
_, err = cli.Do(req)
71+
require.NoError(t, err)
72+
require.NotNil(t, gotReq)
73+
74+
origReq := gotReq
75+
require.NotEmpty(t, origReq.Header.Get("Authorization"))
76+
require.NotEmpty(t, origReq.Header.Get("X-Amz-Date"))
77+
78+
// Perform the same request but with a header that shouldn't included in the
79+
// signature; validate that the Authorization signature matches.
80+
t.Run("Ignored Headers", func(t *testing.T) {
81+
req, err := http.NewRequest(http.MethodPost, "google.com", strings.NewReader("Hello, world!"))
82+
require.NoError(t, err)
83+
84+
req.Header.Add("Uber-Trace-Id", "some-trace-id")
85+
86+
_, err = cli.Do(req)
87+
require.NoError(t, err)
88+
require.NotNil(t, gotReq)
89+
90+
require.Equal(t, origReq.Header.Get("Authorization"), gotReq.Header.Get("Authorization"))
91+
})
92+
}

sigv4/testdata/sigv4_bad.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
region: us-east-2
2+
access_key: AccessKey
3+
profile: profile
4+
role_arn: blah:role/arn

0 commit comments

Comments
 (0)