Skip to content

Commit 7e586d2

Browse files
filters/auth: add setRequestHeaderFromSecret filter (zalando#2740)
Add a filter to set request header value from secret with optional prefix and suffix. It is similar to `bearerinjector` which is equivalent to `setRequestHeaderFromSecret("Authorization", "/tokens/my-token", "Bearer ")` For zalando#1952 Signed-off-by: Alexander Yastrebov <[email protected]>
1 parent 70cb998 commit 7e586d2

File tree

5 files changed

+215
-13
lines changed

5 files changed

+215
-13
lines changed

docs/reference/filters.md

+36-13
Original file line numberDiff line numberDiff line change
@@ -2957,31 +2957,54 @@ the -rfc-patch-path flag. See
29572957
[URI standards interpretation](../operation/operation.md#uri-standards-interpretation).
29582958
29592959
## Egress
2960-
### bearerinjector
29612960
2962-
This filter injects `Bearer` tokens into `Authorization` headers read
2963-
from file providing the token as content. This is only for use cases
2964-
using skipper as sidecar to inject tokens for the application on the
2961+
### setRequestHeaderFromSecret
2962+
2963+
This filter sets request header to the secret value with optional prefix and suffix.
2964+
This is only for use cases using skipper as sidecar to inject tokens for the application on the
29652965
[**egress**](egress.md) path, if it's used in the **ingress** path you likely
29662966
create a security issue for your application.
29672967
29682968
This filter should be used as an [egress](egress.md) only feature.
29692969
2970+
Parameters:
2971+
2972+
* header name (string)
2973+
* secret name (string)
2974+
* value prefix (string) - optional
2975+
* value suffix (string) - optional
2976+
29702977
Example:
29712978
29722979
```
2973-
egress1: Method("POST") && Host("api.example.com") -> bearerinjector("/tmp/secrets/write-token") -> "https://api.example.com/shoes";
2974-
egress2: Method("GET") && Host("api.example.com") -> bearerinjector("/tmp/secrets/read-token") -> "https://api.example.com/shoes";
2980+
egress1: Method("GET") -> setRequestHeaderFromSecret("Authorization", "/tmp/secrets/get-token") -> "https://api.example.com";
2981+
egress2: Method("POST") -> setRequestHeaderFromSecret("Authorization", "/tmp/secrets/post-token", "foo-") -> "https://api.example.com";
2982+
egress3: Method("PUT") -> setRequestHeaderFromSecret("X-Secret", "/tmp/secrets/put-token", "bar-", "-baz") -> "https://api.example.com";
29752983
```
29762984
2977-
To integrate with the `bearerinjector` filter you need to run skipper
2978-
with `-credentials-paths=/tmp/secrets` and specify an update interval
2979-
`-credentials-update-interval=10s`. Files in the credentials path can
2980-
be a directory, which will be able to find all files within this
2981-
directory, but it won't walk subtrees. For the example case, there
2982-
have to be filenames `write-token` and `read-token` within the
2985+
To use `setRequestHeaderFromSecret` filter you need to run skipper
2986+
with `-credentials-paths=/tmp/secrets` and specify an update interval `-credentials-update-interval=10s`.
2987+
Files in the credentials path can be a directory, which will be able to find all files within this
2988+
directory, but it won't walk subtrees.
2989+
For the example case, there have to be `get-token`, `post-token` and `put-token` files within the
29832990
specified credential paths `/tmp/secrets/`, resulting in
2984-
`/tmp/secrets/write-token` and `/tmp/secrets/read-token`.
2991+
`/tmp/secrets/get-token`, `/tmp/secrets/post-token` and `/tmp/secrets/put-token`.
2992+
2993+
### bearerinjector
2994+
2995+
This filter injects `Bearer` tokens into `Authorization` headers read
2996+
from file providing the token as content.
2997+
2998+
It is a special form of `setRequestHeaderFromSecret` with `"Authorization"` header name,
2999+
`"Bearer "` prefix and empty suffix.
3000+
3001+
Example:
3002+
3003+
```
3004+
egress: * -> bearerinjector("/tmp/secrets/my-token") -> "https://api.example.com";
3005+
3006+
// equivalent to setRequestHeaderFromSecret("Authorization", "/tmp/secrets/my-token", "Bearer ")
3007+
```
29853008
29863009
## Open Tracing
29873010
### tracingBaggageToTag

filters/auth/secretheader.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package auth
2+
3+
import (
4+
"github.com/zalando/skipper/filters"
5+
"github.com/zalando/skipper/secrets"
6+
)
7+
8+
type (
9+
secretHeaderSpec struct {
10+
secretsReader secrets.SecretsReader
11+
}
12+
13+
secretHeaderFilter struct {
14+
headerName string
15+
secretName string
16+
prefix string
17+
suffix string
18+
19+
secretsReader secrets.SecretsReader
20+
}
21+
)
22+
23+
func NewSetRequestHeaderFromSecret(sr secrets.SecretsReader) filters.Spec {
24+
return &secretHeaderSpec{secretsReader: sr}
25+
}
26+
27+
func (*secretHeaderSpec) Name() string {
28+
return filters.SetRequestHeaderFromSecretName
29+
}
30+
31+
func (s *secretHeaderSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
32+
if len(args) < 2 || len(args) > 4 {
33+
return nil, filters.ErrInvalidFilterParameters
34+
}
35+
var ok bool
36+
37+
f := &secretHeaderFilter{
38+
secretsReader: s.secretsReader,
39+
}
40+
41+
f.headerName, ok = args[0].(string)
42+
if !ok {
43+
return nil, filters.ErrInvalidFilterParameters
44+
}
45+
46+
f.secretName, ok = args[1].(string)
47+
if !ok {
48+
return nil, filters.ErrInvalidFilterParameters
49+
}
50+
51+
if len(args) > 2 {
52+
f.prefix, ok = args[2].(string)
53+
if !ok {
54+
return nil, filters.ErrInvalidFilterParameters
55+
}
56+
}
57+
58+
if len(args) > 3 {
59+
f.suffix, ok = args[3].(string)
60+
if !ok {
61+
return nil, filters.ErrInvalidFilterParameters
62+
}
63+
}
64+
65+
return f, nil
66+
}
67+
68+
func (f *secretHeaderFilter) Request(ctx filters.FilterContext) {
69+
value, ok := f.secretsReader.GetSecret(f.secretName)
70+
if ok {
71+
ctx.Request().Header.Set(f.headerName, f.prefix+string(value)+f.suffix)
72+
}
73+
}
74+
75+
func (*secretHeaderFilter) Response(filters.FilterContext) {}

filters/auth/secretheader_test.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package auth_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"github.com/zalando/skipper/eskip"
10+
"github.com/zalando/skipper/filters/auth"
11+
"github.com/zalando/skipper/filters/filtertest"
12+
)
13+
14+
type testSecretsReader struct {
15+
name string
16+
secret string
17+
}
18+
19+
func (tsr *testSecretsReader) GetSecret(name string) ([]byte, bool) {
20+
if name == tsr.name {
21+
return []byte(tsr.secret), true
22+
}
23+
return nil, false
24+
}
25+
26+
func (*testSecretsReader) Close() {}
27+
28+
func TestSetRequestHeaderFromSecretInvalidArgs(t *testing.T) {
29+
spec := auth.NewSetRequestHeaderFromSecret(nil)
30+
for _, def := range []string{
31+
`setRequestHeaderFromSecret()`,
32+
`setRequestHeaderFromSecret("X-Secret")`,
33+
`setRequestHeaderFromSecret("X-Secret", 1)`,
34+
`setRequestHeaderFromSecret(1, "/my-secret")`,
35+
`setRequestHeaderFromSecret("X-Secret", "/my-secret", 1)`,
36+
`setRequestHeaderFromSecret("X-Secret", "/my-secret", "prefix", 1)`,
37+
`setRequestHeaderFromSecret("X-Secret", "/my-secret", "prefix", "suffix", "garbage")`,
38+
} {
39+
t.Run(def, func(t *testing.T) {
40+
ff := eskip.MustParseFilters(def)
41+
require.Len(t, ff, 1)
42+
43+
_, err := spec.CreateFilter(ff[0].Args)
44+
assert.Error(t, err)
45+
})
46+
}
47+
}
48+
49+
func TestSetRequestHeaderFromSecret(t *testing.T) {
50+
spec := auth.NewSetRequestHeaderFromSecret(&testSecretsReader{
51+
name: "/my-secret",
52+
secret: "secret-value",
53+
})
54+
55+
assert.Equal(t, "setRequestHeaderFromSecret", spec.Name())
56+
57+
for _, tc := range []struct {
58+
def, header, value string
59+
}{
60+
{
61+
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret")`,
62+
header: "X-Secret",
63+
value: "secret-value",
64+
},
65+
{
66+
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret", "foo-")`,
67+
header: "X-Secret",
68+
value: "foo-secret-value",
69+
},
70+
{
71+
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret", "foo-", "-bar")`,
72+
header: "X-Secret",
73+
value: "foo-secret-value-bar",
74+
},
75+
{
76+
def: `setRequestHeaderFromSecret("X-Secret", "/does-not-exist")`,
77+
header: "X-Secret",
78+
value: "",
79+
},
80+
} {
81+
t.Run(tc.def, func(t *testing.T) {
82+
ff := eskip.MustParseFilters(tc.def)
83+
require.Len(t, ff, 1)
84+
85+
f, err := spec.CreateFilter(ff[0].Args)
86+
assert.NoError(t, err)
87+
88+
ctx := &filtertest.Context{
89+
FRequest: &http.Request{
90+
Header: http.Header{},
91+
},
92+
}
93+
f.Request(ctx)
94+
95+
if tc.value != "" {
96+
assert.Equal(t, tc.value, ctx.FRequest.Header.Get(tc.header))
97+
} else {
98+
assert.NotContains(t, ctx.FRequest.Header, tc.header)
99+
}
100+
})
101+
}
102+
}

filters/filters.go

+1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ const (
333333
RfcPathName = "rfcPath"
334334
RfcHostName = "rfcHost"
335335
BearerInjectorName = "bearerinjector"
336+
SetRequestHeaderFromSecretName = "setRequestHeaderFromSecret"
336337
TracingBaggageToTagName = "tracingBaggageToTag"
337338
StateBagToTagName = "stateBagToTag"
338339
TracingTagName = "tracingTag"

skipper.go

+1
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,7 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
15841584
block.NewBlock(o.MaxMatcherBufferSize),
15851585
block.NewBlockHex(o.MaxMatcherBufferSize),
15861586
auth.NewBearerInjector(sp),
1587+
auth.NewSetRequestHeaderFromSecret(sp),
15871588
auth.NewJwtValidationWithOptions(tio),
15881589
auth.TokenintrospectionWithOptions(auth.NewOAuthTokenintrospectionAnyClaims, tio),
15891590
auth.TokenintrospectionWithOptions(auth.NewOAuthTokenintrospectionAllClaims, tio),

0 commit comments

Comments
 (0)