mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
logging: add support for hashing data (#4434)
* logging: add support for hashing data * Update modules/logging/filters.go Co-authored-by: wiese <wiese@users.noreply.github.com> * Update modules/logging/filters.go Co-authored-by: wiese <wiese@users.noreply.github.com> Co-authored-by: wiese <wiese@users.noreply.github.com>
This commit is contained in:
parent
5bf0adad87
commit
a1b417c832
3 changed files with 100 additions and 13 deletions
|
@ -8,18 +8,21 @@ log {
|
||||||
uri query {
|
uri query {
|
||||||
replace foo REDACTED
|
replace foo REDACTED
|
||||||
delete bar
|
delete bar
|
||||||
|
hash baz
|
||||||
}
|
}
|
||||||
request>headers>Authorization replace REDACTED
|
request>headers>Authorization replace REDACTED
|
||||||
request>headers>Server delete
|
request>headers>Server delete
|
||||||
request>headers>Cookie cookie {
|
request>headers>Cookie cookie {
|
||||||
replace foo REDACTED
|
replace foo REDACTED
|
||||||
delete bar
|
delete bar
|
||||||
|
hash baz
|
||||||
}
|
}
|
||||||
request>remote_ip ip_mask {
|
request>remote_ip ip_mask {
|
||||||
ipv4 24
|
ipv4 24
|
||||||
ipv6 32
|
ipv6 32
|
||||||
}
|
}
|
||||||
request>headers>Regexp regexp secret REDACTED
|
request>headers>Regexp regexp secret REDACTED
|
||||||
|
request>headers>Hash hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,10 +55,17 @@ log {
|
||||||
{
|
{
|
||||||
"name": "bar",
|
"name": "bar",
|
||||||
"type": "delete"
|
"type": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "baz",
|
||||||
|
"type": "hash"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filter": "cookie"
|
"filter": "cookie"
|
||||||
},
|
},
|
||||||
|
"request\u003eheaders\u003eHash": {
|
||||||
|
"filter": "hash"
|
||||||
|
},
|
||||||
"request\u003eheaders\u003eRegexp": {
|
"request\u003eheaders\u003eRegexp": {
|
||||||
"filter": "regexp",
|
"filter": "regexp",
|
||||||
"regexp": "secret",
|
"regexp": "secret",
|
||||||
|
@ -79,6 +89,10 @@ log {
|
||||||
{
|
{
|
||||||
"parameter": "bar",
|
"parameter": "bar",
|
||||||
"type": "delete"
|
"type": "delete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameter": "baz",
|
||||||
|
"type": "hash"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filter": "query"
|
"filter": "query"
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -34,6 +36,7 @@ func init() {
|
||||||
caddy.RegisterModule(QueryFilter{})
|
caddy.RegisterModule(QueryFilter{})
|
||||||
caddy.RegisterModule(CookieFilter{})
|
caddy.RegisterModule(CookieFilter{})
|
||||||
caddy.RegisterModule(RegexpFilter{})
|
caddy.RegisterModule(RegexpFilter{})
|
||||||
|
caddy.RegisterModule(HashFilter{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogFieldFilter can filter (or manipulate)
|
// LogFieldFilter can filter (or manipulate)
|
||||||
|
@ -65,6 +68,35 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hash returns the first 4 bytes of the SHA-256 hash of the given data as hexadecimal
|
||||||
|
func hash(s string) string {
|
||||||
|
return fmt.Sprintf("%.4x", sha256.Sum256([]byte(s)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashFilter is a Caddy log field filter that
|
||||||
|
// replaces the field with the initial 4 bytes of the SHA-256 hash of the content.
|
||||||
|
type HashFilter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaddyModule returns the Caddy module information.
|
||||||
|
func (HashFilter) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
ID: "caddy.logging.encoders.filter.hash",
|
||||||
|
New: func() caddy.Module { return new(HashFilter) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||||
|
func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter filters the input field with the replacement value.
|
||||||
|
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||||
|
in.String = hash(in.String)
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
// ReplaceFilter is a Caddy log field filter that
|
// ReplaceFilter is a Caddy log field filter that
|
||||||
// replaces the field with the indicated string.
|
// replaces the field with the indicated string.
|
||||||
type ReplaceFilter struct {
|
type ReplaceFilter struct {
|
||||||
|
@ -195,15 +227,19 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||||
type filterAction string
|
type filterAction string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Replace value(s) of query parameter(s).
|
// Replace value(s).
|
||||||
replaceAction filterAction = "replace"
|
replaceAction filterAction = "replace"
|
||||||
// Delete query parameter(s).
|
|
||||||
|
// Hash value(s).
|
||||||
|
hashAction filterAction = "hash"
|
||||||
|
|
||||||
|
// Delete.
|
||||||
deleteAction filterAction = "delete"
|
deleteAction filterAction = "delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a filterAction) IsValid() error {
|
func (a filterAction) IsValid() error {
|
||||||
switch a {
|
switch a {
|
||||||
case replaceAction, deleteAction:
|
case replaceAction, deleteAction, hashAction:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +247,7 @@ func (a filterAction) IsValid() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type queryFilterAction struct {
|
type queryFilterAction struct {
|
||||||
// `replace` to replace the value(s) associated with the parameter(s) or `delete` to remove them entirely.
|
// `replace` to replace the value(s) associated with the parameter(s), `hash` to replace them with the 4 initial bytes of the SHA-256 of their content or `delete` to remove them entirely.
|
||||||
Type filterAction `json:"type"`
|
Type filterAction `json:"type"`
|
||||||
|
|
||||||
// The name of the query parameter.
|
// The name of the query parameter.
|
||||||
|
@ -224,9 +260,9 @@ type queryFilterAction struct {
|
||||||
// QueryFilter is a Caddy log field filter that filters
|
// QueryFilter is a Caddy log field filter that filters
|
||||||
// query parameters from a URL.
|
// query parameters from a URL.
|
||||||
//
|
//
|
||||||
// This filter updates the logged URL string to remove or replace query
|
// This filter updates the logged URL string to remove, replace or hash
|
||||||
// parameters containing sensitive data. For instance, it can be used
|
// query parameters containing sensitive data. For instance, it can be
|
||||||
// to redact any kind of secrets which were passed as query parameters,
|
// used to redact any kind of secrets which were passed as query parameters,
|
||||||
// such as OAuth access tokens, session IDs, magic link tokens, etc.
|
// such as OAuth access tokens, session IDs, magic link tokens, etc.
|
||||||
type QueryFilter struct {
|
type QueryFilter struct {
|
||||||
// A list of actions to apply to the query parameters of the URL.
|
// A list of actions to apply to the query parameters of the URL.
|
||||||
|
@ -271,6 +307,14 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
qfa.Value = d.Val()
|
qfa.Value = d.Val()
|
||||||
|
|
||||||
|
case "hash":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
qfa.Type = hashAction
|
||||||
|
qfa.Parameter = d.Val()
|
||||||
|
|
||||||
case "delete":
|
case "delete":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
@ -304,6 +348,11 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||||
q[a.Parameter][i] = a.Value
|
q[a.Parameter][i] = a.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case hashAction:
|
||||||
|
for i := range q[a.Parameter] {
|
||||||
|
q[a.Parameter][i] = hash(a.Value)
|
||||||
|
}
|
||||||
|
|
||||||
case deleteAction:
|
case deleteAction:
|
||||||
q.Del(a.Parameter)
|
q.Del(a.Parameter)
|
||||||
}
|
}
|
||||||
|
@ -316,7 +365,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
type cookieFilterAction struct {
|
type cookieFilterAction struct {
|
||||||
// `replace` to replace the value of the cookie or `delete` to remove it entirely.
|
// `replace` to replace the value of the cookie, `hash` to replace it with the 4 initial bytes of the SHA-256 of its content or `delete` to remove it entirely.
|
||||||
Type filterAction `json:"type"`
|
Type filterAction `json:"type"`
|
||||||
|
|
||||||
// The name of the cookie.
|
// The name of the cookie.
|
||||||
|
@ -330,7 +379,7 @@ type cookieFilterAction struct {
|
||||||
// cookies.
|
// cookies.
|
||||||
//
|
//
|
||||||
// This filter updates the logged HTTP header string
|
// This filter updates the logged HTTP header string
|
||||||
// to remove or replace cookies containing sensitive data. For instance,
|
// to remove, replace or hash cookies containing sensitive data. For instance,
|
||||||
// it can be used to redact any kind of secrets, such as session IDs.
|
// it can be used to redact any kind of secrets, such as session IDs.
|
||||||
//
|
//
|
||||||
// If several actions are configured for the same cookie name, only the first
|
// If several actions are configured for the same cookie name, only the first
|
||||||
|
@ -378,6 +427,14 @@ func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
cfa.Value = d.Val()
|
cfa.Value = d.Val()
|
||||||
|
|
||||||
|
case "hash":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
cfa.Type = hashAction
|
||||||
|
cfa.Name = d.Val()
|
||||||
|
|
||||||
case "delete":
|
case "delete":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
@ -415,6 +472,11 @@ OUTER:
|
||||||
transformedRequest.AddCookie(c)
|
transformedRequest.AddCookie(c)
|
||||||
continue OUTER
|
continue OUTER
|
||||||
|
|
||||||
|
case hashAction:
|
||||||
|
c.Value = hash(c.Value)
|
||||||
|
transformedRequest.AddCookie(c)
|
||||||
|
continue OUTER
|
||||||
|
|
||||||
case deleteAction:
|
case deleteAction:
|
||||||
continue OUTER
|
continue OUTER
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,15 @@ func TestQueryFilter(t *testing.T) {
|
||||||
{replaceAction, "notexist", "REDACTED"},
|
{replaceAction, "notexist", "REDACTED"},
|
||||||
{deleteAction, "bar", ""},
|
{deleteAction, "bar", ""},
|
||||||
{deleteAction, "notexist", ""},
|
{deleteAction, "notexist", ""},
|
||||||
|
{hashAction, "hash", ""},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
if f.Validate() != nil {
|
if f.Validate() != nil {
|
||||||
t.Fatalf("the filter must be valid")
|
t.Fatalf("the filter must be valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"})
|
out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed"})
|
||||||
if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED" {
|
if out.String != "/path?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442" {
|
||||||
t.Fatalf("query parameters have not been filtered: %s", out.String)
|
t.Fatalf("query parameters have not been filtered: %s", out.String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,10 +46,11 @@ func TestCookieFilter(t *testing.T) {
|
||||||
f := CookieFilter{[]cookieFilterAction{
|
f := CookieFilter{[]cookieFilterAction{
|
||||||
{replaceAction, "foo", "REDACTED"},
|
{replaceAction, "foo", "REDACTED"},
|
||||||
{deleteAction, "bar", ""},
|
{deleteAction, "bar", ""},
|
||||||
|
{hashAction, "hash", ""},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e"})
|
out := f.Filter(zapcore.Field{String: "foo=a; foo=b; bar=c; bar=d; baz=e; hash=hashed"})
|
||||||
if out.String != "foo=REDACTED; foo=REDACTED; baz=e" {
|
if out.String != "foo=REDACTED; foo=REDACTED; baz=e; hash=1a06df82" {
|
||||||
t.Fatalf("cookies have not been filtered: %s", out.String)
|
t.Fatalf("cookies have not been filtered: %s", out.String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,3 +80,12 @@ func TestRegexpFilter(t *testing.T) {
|
||||||
t.Fatalf("field has not been filtered: %s", out.String)
|
t.Fatalf("field has not been filtered: %s", out.String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHashFilter(t *testing.T) {
|
||||||
|
f := HashFilter{}
|
||||||
|
|
||||||
|
out := f.Filter(zapcore.Field{String: "foo"})
|
||||||
|
if out.String != "2c26b46b" {
|
||||||
|
t.Fatalf("field has not been filtered: %s", out.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue