Skip to content

Commit

Permalink
filters/auth: add setRequestHeaderFromSecret filter (#2740)
Browse files Browse the repository at this point in the history
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 #1952

Signed-off-by: Alexander Yastrebov <[email protected]>
  • Loading branch information
AlexanderYastrebov authored Nov 16, 2023
1 parent 3e18858 commit ec3881d
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 13 deletions.
49 changes: 36 additions & 13 deletions docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -2957,31 +2957,54 @@ the -rfc-patch-path flag. See
[URI standards interpretation](../operation/operation.md#uri-standards-interpretation).
## Egress
### bearerinjector
This filter injects `Bearer` tokens into `Authorization` headers read
from file providing the token as content. This is only for use cases
using skipper as sidecar to inject tokens for the application on the
### setRequestHeaderFromSecret
This filter sets request header to the secret value with optional prefix and suffix.
This is only for use cases using skipper as sidecar to inject tokens for the application on the
[**egress**](egress.md) path, if it's used in the **ingress** path you likely
create a security issue for your application.
This filter should be used as an [egress](egress.md) only feature.
Parameters:
* header name (string)
* secret name (string)
* value prefix (string) - optional
* value suffix (string) - optional
Example:
```
egress1: Method("POST") && Host("api.example.com") -> bearerinjector("/tmp/secrets/write-token") -> "https://api.example.com/shoes";
egress2: Method("GET") && Host("api.example.com") -> bearerinjector("/tmp/secrets/read-token") -> "https://api.example.com/shoes";
egress1: Method("GET") -> setRequestHeaderFromSecret("Authorization", "/tmp/secrets/get-token") -> "https://api.example.com";
egress2: Method("POST") -> setRequestHeaderFromSecret("Authorization", "/tmp/secrets/post-token", "foo-") -> "https://api.example.com";
egress3: Method("PUT") -> setRequestHeaderFromSecret("X-Secret", "/tmp/secrets/put-token", "bar-", "-baz") -> "https://api.example.com";
```
To integrate with the `bearerinjector` filter you need to run skipper
with `-credentials-paths=/tmp/secrets` and specify an update interval
`-credentials-update-interval=10s`. Files in the credentials path can
be a directory, which will be able to find all files within this
directory, but it won't walk subtrees. For the example case, there
have to be filenames `write-token` and `read-token` within the
To use `setRequestHeaderFromSecret` filter you need to run skipper
with `-credentials-paths=/tmp/secrets` and specify an update interval `-credentials-update-interval=10s`.
Files in the credentials path can be a directory, which will be able to find all files within this
directory, but it won't walk subtrees.
For the example case, there have to be `get-token`, `post-token` and `put-token` files within the
specified credential paths `/tmp/secrets/`, resulting in
`/tmp/secrets/write-token` and `/tmp/secrets/read-token`.
`/tmp/secrets/get-token`, `/tmp/secrets/post-token` and `/tmp/secrets/put-token`.
### bearerinjector
This filter injects `Bearer` tokens into `Authorization` headers read
from file providing the token as content.
It is a special form of `setRequestHeaderFromSecret` with `"Authorization"` header name,
`"Bearer "` prefix and empty suffix.
Example:
```
egress: * -> bearerinjector("/tmp/secrets/my-token") -> "https://api.example.com";

// equivalent to setRequestHeaderFromSecret("Authorization", "/tmp/secrets/my-token", "Bearer ")
```
## Open Tracing
### tracingBaggageToTag
Expand Down
75 changes: 75 additions & 0 deletions filters/auth/secretheader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package auth

import (
"github.com/zalando/skipper/filters"
"github.com/zalando/skipper/secrets"
)

type (
secretHeaderSpec struct {
secretsReader secrets.SecretsReader
}

secretHeaderFilter struct {
headerName string
secretName string
prefix string
suffix string

secretsReader secrets.SecretsReader
}
)

func NewSetRequestHeaderFromSecret(sr secrets.SecretsReader) filters.Spec {
return &secretHeaderSpec{secretsReader: sr}
}

func (*secretHeaderSpec) Name() string {
return filters.SetRequestHeaderFromSecretName
}

func (s *secretHeaderSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
if len(args) < 2 || len(args) > 4 {
return nil, filters.ErrInvalidFilterParameters
}
var ok bool

f := &secretHeaderFilter{
secretsReader: s.secretsReader,
}

f.headerName, ok = args[0].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}

f.secretName, ok = args[1].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}

if len(args) > 2 {
f.prefix, ok = args[2].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}
}

if len(args) > 3 {
f.suffix, ok = args[3].(string)
if !ok {
return nil, filters.ErrInvalidFilterParameters
}
}

return f, nil
}

func (f *secretHeaderFilter) Request(ctx filters.FilterContext) {
value, ok := f.secretsReader.GetSecret(f.secretName)
if ok {
ctx.Request().Header.Set(f.headerName, f.prefix+string(value)+f.suffix)
}
}

func (*secretHeaderFilter) Response(filters.FilterContext) {}
102 changes: 102 additions & 0 deletions filters/auth/secretheader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package auth_test

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/skipper/eskip"
"github.com/zalando/skipper/filters/auth"
"github.com/zalando/skipper/filters/filtertest"
)

type testSecretsReader struct {
name string
secret string
}

func (tsr *testSecretsReader) GetSecret(name string) ([]byte, bool) {
if name == tsr.name {
return []byte(tsr.secret), true
}
return nil, false
}

func (*testSecretsReader) Close() {}

func TestSetRequestHeaderFromSecretInvalidArgs(t *testing.T) {
spec := auth.NewSetRequestHeaderFromSecret(nil)
for _, def := range []string{
`setRequestHeaderFromSecret()`,
`setRequestHeaderFromSecret("X-Secret")`,
`setRequestHeaderFromSecret("X-Secret", 1)`,
`setRequestHeaderFromSecret(1, "/my-secret")`,
`setRequestHeaderFromSecret("X-Secret", "/my-secret", 1)`,
`setRequestHeaderFromSecret("X-Secret", "/my-secret", "prefix", 1)`,
`setRequestHeaderFromSecret("X-Secret", "/my-secret", "prefix", "suffix", "garbage")`,
} {
t.Run(def, func(t *testing.T) {
ff := eskip.MustParseFilters(def)
require.Len(t, ff, 1)

_, err := spec.CreateFilter(ff[0].Args)
assert.Error(t, err)
})
}
}

func TestSetRequestHeaderFromSecret(t *testing.T) {
spec := auth.NewSetRequestHeaderFromSecret(&testSecretsReader{
name: "/my-secret",
secret: "secret-value",
})

assert.Equal(t, "setRequestHeaderFromSecret", spec.Name())

for _, tc := range []struct {
def, header, value string
}{
{
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret")`,
header: "X-Secret",
value: "secret-value",
},
{
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret", "foo-")`,
header: "X-Secret",
value: "foo-secret-value",
},
{
def: `setRequestHeaderFromSecret("X-Secret", "/my-secret", "foo-", "-bar")`,
header: "X-Secret",
value: "foo-secret-value-bar",
},
{
def: `setRequestHeaderFromSecret("X-Secret", "/does-not-exist")`,
header: "X-Secret",
value: "",
},
} {
t.Run(tc.def, func(t *testing.T) {
ff := eskip.MustParseFilters(tc.def)
require.Len(t, ff, 1)

f, err := spec.CreateFilter(ff[0].Args)
assert.NoError(t, err)

ctx := &filtertest.Context{
FRequest: &http.Request{
Header: http.Header{},
},
}
f.Request(ctx)

if tc.value != "" {
assert.Equal(t, tc.value, ctx.FRequest.Header.Get(tc.header))
} else {
assert.NotContains(t, ctx.FRequest.Header, tc.header)
}
})
}
}
1 change: 1 addition & 0 deletions filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ const (
RfcPathName = "rfcPath"
RfcHostName = "rfcHost"
BearerInjectorName = "bearerinjector"
SetRequestHeaderFromSecretName = "setRequestHeaderFromSecret"
TracingBaggageToTagName = "tracingBaggageToTag"
StateBagToTagName = "stateBagToTag"
TracingTagName = "tracingTag"
Expand Down
1 change: 1 addition & 0 deletions skipper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,7 @@ func run(o Options, sig chan os.Signal, idleConnsCH chan struct{}) error {
block.NewBlock(o.MaxMatcherBufferSize),
block.NewBlockHex(o.MaxMatcherBufferSize),
auth.NewBearerInjector(sp),
auth.NewSetRequestHeaderFromSecret(sp),
auth.NewJwtValidationWithOptions(tio),
auth.TokenintrospectionWithOptions(auth.NewOAuthTokenintrospectionAnyClaims, tio),
auth.TokenintrospectionWithOptions(auth.NewOAuthTokenintrospectionAllClaims, tio),
Expand Down

0 comments on commit ec3881d

Please sign in to comment.