-
Notifications
You must be signed in to change notification settings - Fork 351
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
Adds HeaderSHA256
predicate
#1905
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -378,6 +378,78 @@ JWTPayloadAllKVRegexp("iss", "^https://") | |
JWTPayloadAnyKVRegexp("iss", "^https://") | ||
``` | ||
|
||
### HeaderSHA256 | ||
|
||
Matches if SHA-256 hash of the header value (known as [pre-shared key](https://en.wikipedia.org/wiki/Pre-shared_key) or secret) | ||
equals to any of the configured hash values. | ||
Several hash values could be used to match multiple secrets e.g. during secret rotation. | ||
|
||
Hash values only hide secrets from parties that have access to the source of Skipper routes. | ||
Authentication strength depends on the strength of the secret value so e.g. | ||
`HeaderSHA256("X-Secret", "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b")` | ||
is not stronger than just `Header("X-Secret", "secret")`. | ||
Comment on lines
+387
to
+390
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. discuss: add salt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added guide on how to generate secure header value. In general proper salt value has its own requirements so the idea is to get rid of it if possible to minimize number of parameters that users need to specify and understand. |
||
|
||
The secret value must be kept secret, must be used by a single client and must be rotated periodically. | ||
See below how to generate random secret value using [OpenSSL](https://www.openssl.org/docs/man1.1.1/man1/rand.html). | ||
|
||
Parameters: | ||
|
||
* header name (string) | ||
* one or more hex-encoded SHA-256 hashes of the matching header values (string) | ||
|
||
Secure secret value example: | ||
```sh | ||
# | ||
# 1. Generate cryptographically secure pseudo random secret header value: | ||
# - length of at least 32 bytes (the size of the SHA-256 output) | ||
# - encode as -base64 or -hex to get ASCII text value | ||
# | ||
SECRET=$(openssl rand -base64 32) | ||
echo $SECRET | ||
3YchPsliGjBXvyl/ncLWEI8/loKGrj/VNM4garxWEmA= | ||
|
||
# | ||
# 2. Get SHA-256 hash of the secret header value to use as HeaderSHA256 argument: | ||
# - use echo -n to not output the trailing newline | ||
# | ||
echo -n $SECRET | sha256sum | ||
a6131ba920df753c8109500cc11818f7192336d06532f6fa13009c2e4f6e1841 - | ||
``` | ||
``` | ||
// 3. Configure route to match hash of the secret value | ||
HeaderSHA256( | ||
"X-Secret", | ||
"a6131ba920df753c8109500cc11818f7192336d06532f6fa13009c2e4f6e1841" | ||
) -> inlineContent("ok\n") -> <shunt> | ||
``` | ||
```sh | ||
# 4. Test secret value | ||
curl -H "X-Secret: $SECRET" http://localhost:9090 | ||
``` | ||
|
||
Secret rotation example: | ||
``` | ||
// To rotate secret: | ||
// * add new secret - both old and new secrets match during rotation | ||
// * update client to use new secret | ||
// * remove old secret | ||
HeaderSHA256( | ||
"X-Secret", | ||
"cba06b5736faf67e54b07b561eae94395e774c517a7d910a54369e1263ccfbd4", // SHA256("old") | ||
"11507a0e2f5e69d5dfa40a62a1bd7b6ee57e6bcd85c67c9b8431b36fff21c437" // SHA256("new") | ||
) -> inlineContent("ok\n") -> <shunt> | ||
``` | ||
|
||
[Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) example: | ||
``` | ||
anon: * -> setResponseHeader("WWW-Authenticate", `Basic realm="foo", charset="UTF-8"`) -> status(401) -> <shunt>; | ||
auth: HeaderSHA256( | ||
"Authorization", | ||
"caae07e42ed8d231a58edcde95782b0feb67186172c18c89894ce4c2174df137", // SHA256("Basic " + BASE64("test:123£")) | ||
"157da8472590f0ce0a7c651bd79aecb5cc582944fcf76cbabada915d333deee8" // SHA256("Basic " + BASE64("Aladdin:open sesame")) | ||
) -> inlineContent("ok\n") -> <shunt>; | ||
``` | ||
|
||
## Interval | ||
|
||
An interval implements custom predicates to match routes only during some period of time. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package auth | ||
|
||
import ( | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"net/http" | ||
|
||
"github.com/zalando/skipper/predicates" | ||
"github.com/zalando/skipper/routing" | ||
) | ||
|
||
type headerSha256Spec struct{} | ||
|
||
type headerSha256Predicate struct { | ||
name string | ||
hashes [][sha256.Size]byte | ||
} | ||
|
||
// NewHeaderSHA256 creates a predicate specification, whose instances match SHA-256 hash of the header value. | ||
// The HeaderSHA256 predicate requires the header name and one or more hex-encoded SHA-256 hash values of the matching header. | ||
func NewHeaderSHA256() routing.PredicateSpec { | ||
return &headerSha256Spec{} | ||
} | ||
|
||
func (*headerSha256Spec) Name() string { | ||
return predicates.HeaderSHA256Name | ||
} | ||
|
||
// Create a predicate instance matching SHA256 hash of the header value | ||
func (*headerSha256Spec) Create(args []interface{}) (routing.Predicate, error) { | ||
if len(args) < 2 { | ||
return nil, predicates.ErrInvalidPredicateParameters | ||
} | ||
|
||
name, ok := args[0].(string) | ||
if !ok { | ||
return nil, predicates.ErrInvalidPredicateParameters | ||
} | ||
|
||
var hashes [][sha256.Size]byte | ||
for _, arg := range args[1:] { | ||
hexHash, ok := arg.(string) | ||
if !ok { | ||
return nil, predicates.ErrInvalidPredicateParameters | ||
} | ||
hash, err := hex.DecodeString(hexHash) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(hash) != sha256.Size { | ||
return nil, predicates.ErrInvalidPredicateParameters | ||
} | ||
hashes = append(hashes, *(*[sha256.Size]byte)(hash)) // https://github.com/golang/go/issues/46505 | ||
} | ||
|
||
return &headerSha256Predicate{name, hashes}, nil | ||
} | ||
|
||
func (p *headerSha256Predicate) Match(r *http.Request) bool { | ||
value := r.Header.Get(p.name) | ||
if value == "" { | ||
return false | ||
} | ||
|
||
valueHash := sha256.Sum256([]byte(value)) | ||
|
||
for _, hash := range p.hashes { | ||
if valueHash == hash { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package auth | ||
|
||
import ( | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
func TestHeaderSHA256Args(t *testing.T) { | ||
s := NewHeaderSHA256() | ||
for _, tc := range []struct { | ||
args []interface{} | ||
}{ | ||
{ | ||
args: []interface{}{}, | ||
}, | ||
{ | ||
args: []interface{}{"X-Secret", "xyz"}, | ||
}, | ||
{ | ||
args: []interface{}{"X-Secret", "00112233"}, | ||
}, | ||
{ | ||
args: []interface{}{"X-Secret", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", "AA"}, | ||
}, | ||
} { | ||
if _, err := s.Create(tc.args); err == nil { | ||
t.Errorf("expected error for arguments: %v", tc.args) | ||
} | ||
} | ||
} | ||
|
||
func TestHeaderSHA256Match(t *testing.T) { | ||
s := NewHeaderSHA256() | ||
p, err := s.Create([]interface{}{ | ||
"X-Secret", | ||
"2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b", // "secret" | ||
"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", // "password" | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
for _, tc := range []struct { | ||
header http.Header | ||
match bool | ||
}{ | ||
{ | ||
header: http.Header{ | ||
"X-Test": []string{"foo"}, | ||
}, | ||
match: false, | ||
}, | ||
{ | ||
header: http.Header{ | ||
"X-Secret": []string{"foo"}, | ||
}, | ||
match: false, | ||
}, | ||
{ | ||
header: http.Header{ | ||
"X-Secret": []string{"SECRET"}, | ||
}, | ||
match: false, | ||
}, | ||
{ | ||
header: http.Header{ | ||
"X-Secret": []string{"PASSWORD"}, | ||
}, | ||
match: false, | ||
}, | ||
{ | ||
header: http.Header{ | ||
"X-Secret": []string{"secret"}, | ||
}, | ||
match: true, | ||
}, | ||
{ | ||
header: http.Header{ | ||
"X-Secret": []string{"password"}, | ||
}, | ||
match: true, | ||
}, | ||
} { | ||
if p.Match(&http.Request{Header: tc.header}) != tc.match { | ||
t.Errorf("expected match: %v", tc.match) | ||
} | ||
} | ||
} | ||
|
||
func BenchmarkHeaderSHA256Match(b *testing.B) { | ||
s := NewHeaderSHA256() | ||
p, err := s.Create([]interface{}{"X-Secret", "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b"}) | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
r := &http.Request{Header: http.Header{"X-Secret": []string{"secret"}}} | ||
if !p.Match(r) { | ||
b.Fatal("match expected") | ||
} | ||
b.ResetTimer() | ||
for i := 0; i < b.N; i++ { | ||
p.Match(r) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same could be achieved using two routes with two different
HeaderSHA256
but it is more convenient to use single predicate with multiple secrets to avoid route duplication, e.g. inzalando.org/skipper-predicate
ingress annotation.