From 3986e202bf86c161fe6f90d311841ddce638b132 Mon Sep 17 00:00:00 2001 From: souhailanoor <90720110+souhailanoor@users.noreply.github.com> Date: Fri, 20 May 2022 14:57:05 -0500 Subject: [PATCH] FB-1378: Use IP whitelisting for ingest authentication and authorization (#2070) * Use IP whitelisting for ingest For ingest, use configured IPs to authenticate the requests. Auth-token will no longer be used for requests from ingest consumers. If IP in request is in configured IPs, authenticate and authorize as an admin. If IP in request is not in configured IPs, proceed with the standard authentication/authorization using ADD. * need to remove port from client IP * addressed review comments --- authn/authenticate.go | 71 +++++-- authn/authenticate_internal_test.go | 102 ++++++++++ ctl/import_test.go | 1 + ctl/server.go | 1 + http_handler.go | 151 ++++++++++---- http_handler_internal_test.go | 225 +++++++++++++++++---- http_handler_test.go | 196 ++++++++++++++++++ internal/clustertests/cluster_test.go | 1 + internal_client.go | 6 + internal_client_test.go | 11 + qa/scripts/auth-smoke/tests/sup218-test.sh | 3 +- qa/scripts/utilCluster.sh | 1 + server/config.go | 34 ++++ server/config_internal_test.go | 72 +++++++ server/server.go | 5 +- testdata/certs/localhost.crt | 26 +++ testdata/certs/localhost.key | 28 +++ 17 files changed, 844 insertions(+), 90 deletions(-) create mode 100644 testdata/certs/localhost.crt create mode 100644 testdata/certs/localhost.key diff --git a/authn/authenticate.go b/authn/authenticate.go index 3e3e96928..66d1cfdb5 100644 --- a/authn/authenticate.go +++ b/authn/authenticate.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net" "net/http" "net/url" "strconv" @@ -61,22 +62,23 @@ type Groups struct { // Auth holds state, configuration, and utilities needed for authentication. type Auth struct { - logger logger.Logger - cookieName string - secretKey []byte - groupEndpoint string - logoutEndpoint string - fbURL string // fbURL is the domain featurebase is hosted on, used for post logout redirection - oAuthConfig *oauth2.Config - cacheTTL time.Duration // cacheTTL is used to determine if a cached item should be refreshed or not - tokenTTR time.Duration // tokenTTR (time to refresh) is used to determine if a token should be refreshed or not - tokenCache map[string]cachedToken // tokenCache is a map of accessToken -> *oauth2.Token which we can use to refresh the tokens - groupsCache map[string]cachedGroups // groupsCache is a map of accessToken -> group memberships - lastCacheClean time.Time // last cache clean is the time that the cache was last cleaned + logger logger.Logger + cookieName string + secretKey []byte + groupEndpoint string + logoutEndpoint string + fbURL string // fbURL is the domain featurebase is hosted on, used for post logout redirection + oAuthConfig *oauth2.Config + cacheTTL time.Duration // cacheTTL is used to determine if a cached item should be refreshed or not + tokenTTR time.Duration // tokenTTR (time to refresh) is used to determine if a token should be refreshed or not + tokenCache map[string]cachedToken // tokenCache is a map of accessToken -> *oauth2.Token which we can use to refresh the tokens + groupsCache map[string]cachedGroups // groupsCache is a map of accessToken -> group memberships + lastCacheClean time.Time // last cache clean is the time that the cache was last cleaned + allowedNetworks []net.IPNet // list of allowed networks for ingest } // NewAuth instantiates and returns a new Auth struct -func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenURL, groupEndpoint, logout, clientID, clientSecret, secretKey string) (auth *Auth, err error) { +func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenURL, groupEndpoint, logout, clientID, clientSecret, secretKey string, configuredIPs []string) (auth *Auth, err error) { auth = &Auth{ logger: logger, cookieName: CookieName, @@ -103,6 +105,13 @@ func NewAuth(logger logger.Logger, url string, scopes []string, authURL, tokenUR if auth.secretKey, err = decodeHex(secretKey); err != nil { return nil, errors.Wrap(err, "decoding secret key") } + + // convert IPs and add them to allowed networks + err = auth.convertIP(configuredIPs) + if err != nil { + return nil, err + } + return auth, nil } @@ -336,3 +345,39 @@ func decodeHex(hexstr string) ([]byte, error) { } return data, nil } + +func (a *Auth) convertIP(configuredIPs []string) error { + sz := len(configuredIPs) + nets := make([]net.IPNet, sz) + for i, ip := range configuredIPs { + // skip empty strings + if ip == "" { + sz-- + continue + } + // for IPs passed without a subnet, append /32 to only allow 1 IP + // this step is needed because ParseCIDR method assumes a CIDR address + if !strings.Contains(ip, "/") { + ip = ip + "/32" + } + _, subnet, err := net.ParseCIDR(ip) + if err != nil { + return errors.Wrapf(err, "parsing CIDR for %v", ip) + } + nets[i] = *subnet + } + a.allowedNetworks = nets[:sz] + return nil +} + +// if IP is in allowed networks, then return true to grant admin permissions +func (a *Auth) CheckAllowedNetworks(clientIP string) bool { + clientIP = strings.Split(clientIP, ":")[0] + convertedIP := net.ParseIP(clientIP) + for _, network := range a.allowedNetworks { + if network.Contains(convertedIP) { + return true + } + } + return false +} diff --git a/authn/authenticate_internal_test.go b/authn/authenticate_internal_test.go index 67aa6d695..10dd307fb 100644 --- a/authn/authenticate_internal_test.go +++ b/authn/authenticate_internal_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "net" "net/http" "net/http/httptest" "os" @@ -33,6 +34,7 @@ func NewTestAuth(t *testing.T) *Auth { LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + configuredIPs = []string{} ) a, err := NewAuth( @@ -46,6 +48,7 @@ func NewTestAuth(t *testing.T) *Auth { ClientID, ClientSecret, Key, + configuredIPs, ) if err != nil { t.Fatalf("building auth object%s", err) @@ -147,6 +150,7 @@ func TestAuth(t *testing.T) { "e9088663-eb08-41d7-8f65-efb5f54bbb71", "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", "DEADBEEFD", + []string{}, ) if err == nil || !strings.Contains(err.Error(), "decoding secret key") { t.Fatalf("expected error decoding secret key got: %v", err) @@ -648,3 +652,101 @@ func (s *ServerTransportStream) SetTrailer(md metadata.MD) error { _ = md return nil } + +func TestCheckAllowedNetworks(t *testing.T) { + + tests := []struct { + requestIP string + configuredIPs []string + isAdmin bool + }{ + { + requestIP: "10.0.0.1", + configuredIPs: []string{"10.0.0.1"}, + isAdmin: true, + }, + { + requestIP: "10.0.0.3", + configuredIPs: []string{"10.0.0.1", "10.0.0.2"}, + isAdmin: false, + }, + { + requestIP: "10.0.0.2", + configuredIPs: []string{"10.0.0.1/30"}, + isAdmin: true, + }, + // it is possible for the client IP to have a port + { + requestIP: "10.0.0.2:22", + configuredIPs: []string{"10.0.0.1/30"}, + isAdmin: true, + }, + { + requestIP: "10.1.0.3", + configuredIPs: []string{"10.0.0.1/32"}, + isAdmin: false, + }, + { + requestIP: "10.0.0.254", + configuredIPs: []string{"10.0.0.1/24"}, + isAdmin: true, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("network-%d", i), func(t *testing.T) { + a := NewTestAuth(t) + if err := a.convertIP(test.configuredIPs); err != nil { + t.Fatalf("failed to convert IPs from strings to net.IP: %v", err) + } + got := a.CheckAllowedNetworks(test.requestIP) + if got != test.isAdmin { + t.Fatalf("expected %v, got %v", test.isAdmin, got) + } + }) + } +} + +func TestConvertIP(t *testing.T) { + + tests := []struct { + configuredIPs []string + convertedIPs []net.IPNet + }{ + { + configuredIPs: []string{"10.0.0.1"}, + convertedIPs: []net.IPNet{ + {IP: net.ParseIP("10.0.0.1"), Mask: net.CIDRMask(32, 32)}, + }, + }, + { + configuredIPs: []string{"10.0.0.1/30"}, + convertedIPs: []net.IPNet{ + {IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(30, 32)}, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("network-%d", i), func(t *testing.T) { + a := NewTestAuth(t) + if err := a.convertIP(test.configuredIPs); err != nil { + t.Fatalf("failed to convert IPs from strings to net.IP: %v", err) + } + + if len(a.allowedNetworks) != len(test.convertedIPs) { + t.Fatalf("expected len of %v networks, got %v", len(test.convertedIPs), len(a.allowedNetworks)) + } + + for i := range a.allowedNetworks { + expected, got := test.convertedIPs[i], a.allowedNetworks[i] + if got.IP.String() != expected.IP.String() { + t.Fatalf("for IP, expected %v, got %v", expected.IP, got.IP) + } + if got.Mask.String() != expected.Mask.String() { + t.Fatalf("for mask, expected %v, got %v", expected.Mask.String(), got.Mask.String()) + } + } + }) + } +} diff --git a/ctl/import_test.go b/ctl/import_test.go index 5c4603c89..1aca06ad3 100644 --- a/ctl/import_test.go +++ b/ctl/import_test.go @@ -641,6 +641,7 @@ func TestImport_AuthOn(t *testing.T) { auth.ClientId, auth.ClientSecret, auth.SecretKey, + []string{}, ) if err != nil { t.Fatal(err) diff --git a/ctl/server.go b/ctl/server.go index 20363bced..cb317c6af 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -108,6 +108,7 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { flags.StringVar(&srv.Config.Auth.SecretKey, "auth.secret-key", srv.Config.Auth.SecretKey, "Secret key used for auth.") flags.StringVar(&srv.Config.Auth.PermissionsFile, "auth.permissions", srv.Config.Auth.PermissionsFile, "Permissions' file with group authorization.") flags.StringVar(&srv.Config.Auth.QueryLogPath, "auth.query-log-path", srv.Config.Auth.QueryLogPath, "Path to log user queries") + flags.StringSliceVar(&srv.Config.Auth.ConfiguredIPs, "auth.configured-ips", srv.Config.Auth.ConfiguredIPs, "List of configured IPs allowed for ingest") flags.BoolVar(&srv.Config.DataDog.Enable, "datadog.enable", false, "enable continuous profiling with DataDog cloud service, Note you must have DataDog agent installed") flags.StringVar(&srv.Config.DataDog.Service, "datadog.service", "default-service", "The Datadog service name, for example my-web-app") diff --git a/http_handler.go b/http_handler.go index 8d96a9a80..7ba2f5b4e 100644 --- a/http_handler.go +++ b/http_handler.go @@ -93,6 +93,23 @@ var externalPrefixFlag = map[string]bool{ "version": true, } +const ( + // OriginalIPHeader is the original IP for client + // It is used mainly for authenticating on remote nodes + // ForwardedIPHeader gets updated to the node's IP + // when requests are forward to other nodes in the cluster + OriginalIPHeader = "X-Molecula-Original-IP" + + // ForwardedIPHeader is part of the standard header + // it is used to identify the originating IP of a client + ForwardedIPHeader = "X-Forwarded-For" + + // AllowedNetworksGroupName is used for the admin group authorization + // when authentication is completed through checking the client IP + // against the allowed networks + AllowedNetworksGroupName = "allowed-networks" +) + type errorResponse struct { Error string `json:"error"` } @@ -581,35 +598,69 @@ func (h *Handler) chkInternal(handler http.HandlerFunc) http.HandlerFunc { } } +func (h *Handler) chkAllowedNetworks(r *http.Request) (bool, context.Context) { + // for every request, get IP of the request and check against configured IPs + reqIP := GetIP(r) + if reqIP == "" { + return false, r.Context() + } + + // if client IP is in allowed networks + // add it to the context for key X-Molecula-Original-IP + if h.auth.CheckAllowedNetworks(reqIP) { + ctx := context.WithValue(r.Context(), OriginalIPHeader, reqIP) + return true, ctx + } + return false, r.Context() +} + func (h *Handler) chkAuthN(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() if h.auth != nil { - uinfo, err := h.auth.Authenticate(r.Context(), getToken(r)) + // if IP is in allowed networks, then serve the request + allowedNetwork, ctx := h.chkAllowedNetworks(r) + if allowedNetwork { + handler.ServeHTTP(w, r.WithContext(ctx)) + return + } + + uinfo, err := h.auth.Authenticate(ctx, getToken(r)) if err != nil { http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusUnauthorized) return } // just in case it got refreshed h.auth.SetCookie(w, uinfo.Token, uinfo.Expiry) + ctx = context.WithValue(ctx, "token", r.Header["Authorization"]) } - ctx := context.WithValue(r.Context(), "token", r.Header["Authorization"]) handler.ServeHTTP(w, r.WithContext(ctx)) } } func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // if auth isn't turned on, just serve the request if h.auth == nil { handler.ServeHTTP(w, r) return } + // check if IP is in allowed networks, if yes give it admin permissions + allowedNetwork, ctx := h.chkAllowedNetworks(r) + if allowedNetwork { + ctx = context.WithValue(ctx, contextKeyGroupMembership, []string{AllowedNetworksGroupName, h.permissions.Admin}) + handler.ServeHTTP(w, r.WithContext(ctx)) + return + } + // make a copy of the requested permissions lperm := perm // check if the user is authenticated - uinfo, err := h.auth.Authenticate(r.Context(), getToken(r)) + uinfo, err := h.auth.Authenticate(ctx, getToken(r)) if err != nil { http.Error(w, errors.Wrap(err, "authenticating").Error(), http.StatusForbidden) return @@ -618,7 +669,7 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http h.auth.SetCookie(w, uinfo.Token, uinfo.Expiry) // put the user's groups in the context - ctx := context.WithValue(r.Context(), contextKeyGroupMembership, uinfo.Groups) + ctx = context.WithValue(ctx, contextKeyGroupMembership, uinfo.Groups) ctx = context.WithValue(ctx, "token", "Bearer "+uinfo.Token) // unlikely h.permissions will be nil, but we'll check to be safe @@ -630,7 +681,7 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http // figure out what the user is querying for queryString := "" - queryRequest := r.Context().Value(contextKeyQueryRequest) + queryRequest := ctx.Value(contextKeyQueryRequest) if req, ok := queryRequest.(*QueryRequest); ok { queryString = req.Query @@ -701,10 +752,27 @@ func (h *Handler) chkAuthZ(handler http.HandlerFunc, perm authz.Permission) http } func GetIP(r *http.Request) string { - forwarded := r.Header.Get("X-FORWARDED-FOR") - if forwarded != "" { - return forwarded + // check if original IP was set in the request + og := r.Header.Get(OriginalIPHeader) + if og != "" { + return og } + + // check if original IP is in the context + if ogIP, ok := r.Context().Value(OriginalIPHeader).(string); ok && ogIP != "" { + return ogIP + } + + // X-Forwarded-For can have multiple IPs + // the first IP will always be the originating client IP + // the remaining IPs will be for any proxies the request went through + forwarded := r.Header.Get(ForwardedIPHeader) + forwardedList := strings.Split(forwarded, ",") + if forwardedList[0] != "" { + return forwardedList[0] + + } + return r.RemoteAddr } @@ -888,6 +956,40 @@ func headerAcceptRoaringRow(header http.Header) bool { return false } +func (h *Handler) filterSchema(schema []*IndexInfo, g []authn.Group) []*IndexInfo { + if !h.permissions.IsAdmin(g) { + var filtered []*IndexInfo + allowed := h.permissions.GetAuthorizedIndexList(g, authz.Read) + for _, s := range schema { + for _, index := range allowed { + if s.Name == index { + filtered = append(filtered, s) + break + } + } + } + schema = filtered + } + return schema +} + +func (h *Handler) getGroupMembership(r *http.Request) (g []authn.Group) { + // if IP is in allowed networks, then give admin group membership + if h.auth.CheckAllowedNetworks(GetIP(r)) { + g = []authn.Group{ + { + GroupID: h.permissions.Admin, + GroupName: AllowedNetworksGroupName, + }, + } + return g + } + + // check if group membership was already set in request when auth token was obtained + g = r.Context().Value(contextKeyGroupMembership).([]authn.Group) + return g +} + // handleGetSchema handles GET /schema requests. func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) { if !validHeaderAcceptJSON(r.Header) { @@ -906,24 +1008,12 @@ func (h *Handler) handleGetSchema(w http.ResponseWriter, r *http.Request) { // if auth is turned on, filter response to only include authorized indexes if h.auth != nil { - g := r.Context().Value(contextKeyGroupMembership) + g := h.getGroupMembership(r) if g == nil { http.Error(w, "Forbidden", http.StatusForbidden) return } - if !h.permissions.IsAdmin(g.([]authn.Group)) { - var filtered []*IndexInfo - allowed := h.permissions.GetAuthorizedIndexList(g.([]authn.Group), authz.Read) - for _, s := range schema { - for _, index := range allowed { - if s.Name == index { - filtered = append(filtered, s) - break - } - } - } - schema = filtered - } + schema = h.filterSchema(schema, g) } if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil { @@ -952,25 +1042,14 @@ func (h *Handler) handleGetSchemaDetails(w http.ResponseWriter, r *http.Request) // if auth is turned on, filter response to only include authorized indexes if h.auth != nil { - g := r.Context().Value(contextKeyGroupMembership) + g := h.getGroupMembership(r) if g == nil { http.Error(w, "Forbidden", http.StatusForbidden) return } - if !h.permissions.IsAdmin(g.([]authn.Group)) { - var filtered []*IndexInfo - allowed := h.permissions.GetAuthorizedIndexList(g.([]authn.Group), authz.Read) - for _, s := range schema { - for _, index := range allowed { - if s.Name == index { - filtered = append(filtered, s) - break - } - } - } - schema = filtered - } + schema = h.filterSchema(schema, g) } + if err := json.NewEncoder(w).Encode(Schema{Indexes: schema}); err != nil { h.logger.Printf("write schema response error: %s", err) } diff --git a/http_handler_internal_test.go b/http_handler_internal_test.go index f5237deaa..14129bb4b 100644 --- a/http_handler_internal_test.go +++ b/http_handler_internal_test.go @@ -3,6 +3,7 @@ package pilosa import ( "bytes" + "context" "encoding/hex" "encoding/json" "fmt" @@ -188,6 +189,19 @@ func readResponse(w *httptest.ResponseRecorder) ([]byte, error) { return ioutil.ReadAll(res.Body) } +// common variables used for testing auth +var ( + ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71" + ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" + TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" + GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" + LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" + Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} + Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + ConfiguredIPs = []string{} +) + func TestAuthentication(t *testing.T) { type evaluate func(w *httptest.ResponseRecorder, data []byte) type endpoint func(w http.ResponseWriter, r *http.Request) @@ -221,34 +235,10 @@ func TestAuthentication(t *testing.T) { })) defer groupSrv.Close() - var ( - ClientId = "e9088663-eb08-41d7-8f65-efb5f54bbb71" - ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" - AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" - TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" - GroupEndpointURL = groupSrv.URL - LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" - Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} - SecretKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" - ) + GroupEndpointURL = groupSrv.URL + secretKey, _ := hex.DecodeString(Key) - secretKey, _ := hex.DecodeString("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF") - - a, err := authn.NewAuth( - logger.NewStandardLogger(os.Stdout), - "http://localhost:10101/", - Scopes, - AuthorizeURL, - TokenURL, - GroupEndpointURL, - LogoutURL, - ClientId, - ClientSecret, - SecretKey, - ) - if err != nil { - t.Errorf("building auth object%s", err) - } + a := NewTestAuth(t) h := Handler{ logger: logger.NewStandardLogger(os.Stdout), @@ -781,17 +771,6 @@ func TestChkInternal(t *testing.T) { func NewTestAuth(t *testing.T) *authn.Auth { t.Helper() - var ( - ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71" - ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" - AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" - TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token" - GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" - LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" - Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} - Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" - ) - a, err := authn.NewAuth( logger.NewStandardLogger(os.Stdout), "http://localhost:10101/", @@ -803,6 +782,7 @@ func NewTestAuth(t *testing.T) *authn.Auth { ClientID, ClientSecret, Key, + ConfiguredIPs, ) if err != nil { t.Fatalf("building auth object%s", err) @@ -846,3 +826,172 @@ func TestHandleGetDiskUsage(t *testing.T) { t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode) } } + +func TestAuthzAllowedIPs(t *testing.T) { + tests := []struct { + configuredIPs []string + clientIP string + statusCode int + permission authz.Permission + }{ + // client IP is in configured IP list + { + configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"}, + clientIP: "10.0.0.0", + statusCode: http.StatusOK, + permission: authz.Admin, + }, + // client IP is in configured IP list, testing with CIDR address + { + configuredIPs: []string{"10.0.0.0/30"}, + clientIP: "10.0.0.1", + statusCode: http.StatusOK, + permission: authz.Write, + }, + // client IP has multiple IPs in X-Forwarded-For header + // originating IP is in configured IP list + { + configuredIPs: []string{"10.0.0.0", "10.0.0.1/32", "10.0.0.2"}, + clientIP: "10.0.0.2,10.0.0.255", + statusCode: http.StatusOK, + permission: authz.Read, + }, + // client IP has multiple IPs in X-Forwarded-For header + // originating IP is not in configured IP list + { + configuredIPs: []string{"10.0.0.0/30"}, + clientIP: "10.0.0.255,10.0.0.2", + statusCode: http.StatusForbidden, + permission: authz.Read, + }, + // client IP is not in configured IP list + { + configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"}, + clientIP: "10.0.0.3", + statusCode: http.StatusForbidden, + permission: authz.Write, + }, + // client IP is not in configured IP list + // X-Forwarded-For header is an empty string + { + configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"}, + clientIP: "", + statusCode: http.StatusForbidden, + permission: authz.Write, + }, + } + + testingHandler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("good")) + } + + permissions1 := `"user-groups": + "dca35310-ecda-4f23-86cd-876aee559900": + "test": "write" +admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"` + + for i, test := range tests { + t.Run(fmt.Sprintf("ChkAuthz-%d", i), func(t *testing.T) { + ConfiguredIPs = test.configuredIPs + a := NewTestAuth(t) + + h := Handler{ + logger: logger.NewStandardLogger(os.Stdout), + queryLogger: logger.NewStandardLogger(os.Stdout), + auth: a, + } + + var p authz.GroupPermissions + if err := p.ReadPermissionsFile(strings.NewReader(permissions1)); err != nil { + t.Errorf("Error: %s", err) + } + h.permissions = &p + + r := httptest.NewRequest("GET", "/index/authz-abcd", nil) + r.Header.Set(ForwardedIPHeader, test.clientIP) + w := httptest.NewRecorder() + + handler := h.chkAuthZ(testingHandler, authz.Read) + handler(w, r) + resp := w.Result() + if resp.StatusCode != test.statusCode { + t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode) + } + }) + } +} + +func TestAuthnAllowedIPs(t *testing.T) { + IPList := []string{"10.0.0.0", "10.0.0.1", "10.0.0.2"} + ValidForwardedIP := "10.0.0.0, 10.0.0.3, 10.0.0.4" + InvalidForwardedIP := "10.0.0.3, 10.0.0.4" + + tests := []struct { + configuredIPs []string + clientIP string + statusCode int + secretKey string + }{ + // test client IP was in configured IP list - happy path + { + configuredIPs: IPList, + clientIP: IPList[0], + statusCode: http.StatusOK, + }, + // test empty configured IP list + { + configuredIPs: []string{""}, + clientIP: IPList[0], + statusCode: http.StatusUnauthorized, + }, + // test client IP is not in configured IP list + { + configuredIPs: IPList, + clientIP: "10.0.0.4", + statusCode: http.StatusUnauthorized, + }, + // test multiple client IPs in X-forwarded-IP + // originating IP is in configured IP list + { + configuredIPs: IPList, + clientIP: ValidForwardedIP, + statusCode: http.StatusOK, + }, + // test multiple client IPs in X-forwarded-IP + // originating IP is not in configured IP list + { + configuredIPs: IPList, + clientIP: InvalidForwardedIP, + statusCode: http.StatusUnauthorized, + }, + } + + testingHandler := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("good")) + } + + for i, test := range tests { + t.Run(fmt.Sprintf("ChkAuthn-%d", i), func(t *testing.T) { + ConfiguredIPs = test.configuredIPs + a := NewTestAuth(t) + + h := Handler{ + logger: logger.NewStandardLogger(os.Stdout), + queryLogger: logger.NewStandardLogger(os.Stdout), + auth: a, + } + + r := httptest.NewRequest("GET", "/index/authn-abcd", nil) + r.Header.Set(ForwardedIPHeader, test.clientIP) + r = r.WithContext(context.Background()) + w := httptest.NewRecorder() + + handler := h.chkAuthN(testingHandler) + handler(w, r) + resp := w.Result() + if resp.StatusCode != test.statusCode { + t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode) + } + }) + } +} diff --git a/http_handler_test.go b/http_handler_test.go index b246b6d69..7f720ab60 100644 --- a/http_handler_test.go +++ b/http_handler_test.go @@ -5,8 +5,12 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" "net" + "net/http" gohttp "net/http" + "os" + "path" "reflect" "sort" "strings" @@ -469,3 +473,195 @@ func TestTranslationHandlers(t *testing.T) { } } } + +func TestAuthAllowedNetworks(t *testing.T) { + permissions1 := ` +"user-groups": + "dca35310-ecda-4f23-86cd-876aee55906b": + "test": "read" +admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"` + + tmpDir := t.TempDir() + permissionsPath := path.Join(tmpDir, "test-permissions.yaml") + err := ioutil.WriteFile(permissionsPath, []byte(permissions1), 0666) + if err != nil { + t.Fatalf("failed to write permissions file: %v", err) + } + + queryLogPath := path.Join(tmpDir, "query.log") + _, err = os.Create(queryLogPath) + if err != nil { + t.Fatal(err) + } + + validIP := "10.0.0.2" + + clusterSize := 3 + commandOpts := make([][]server.CommandOption, clusterSize) + configs := make([]*server.Config, clusterSize) + for i := range configs { + conf := server.NewConfig() + configs[i] = conf + conf.TLS.CertificatePath = "./testdata/certs/localhost.crt" + conf.TLS.CertificateKeyPath = "./testdata/certs/localhost.key" + conf.Auth.Enable = true + conf.Auth.ClientId = "e9088663-eb08-41d7-8f65-efb5f54bbb71" + conf.Auth.ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + conf.Auth.AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" + conf.Auth.TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize" + conf.Auth.GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true" + conf.Auth.LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout" + conf.Auth.RedirectBaseURL = "https://localhost:10101/" + conf.Auth.QueryLogPath = queryLogPath + conf.Auth.SecretKey = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + conf.Auth.PermissionsFile = permissionsPath + conf.Auth.Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"} + conf.Auth.ConfiguredIPs = []string{validIP} + commandOpts[i] = append(commandOpts[i], server.OptCommandConfig(conf)) + } + + c := test.MustRunCluster(t, clusterSize, commandOpts...) + defer c.Close() + + m := c.GetPrimary() + index := "allowed-networks-index" + keyedIndex := "allowed-networks-index-keyed" + field := "field1" + + // needed for key translation + nameBytes, err := json.Marshal([]string{"a", "b", "c"}) + if err != nil { + t.Fatalf("marshalling json: %v", err) + } + names := string(nameBytes) + + schema := ` + { + "index-name": "allowed-networks-index-keyed", + "primary-key-type": "string", + "index-action": "create", + "fields": [ + { + "field-name": "stringset", + "field-type": "string", + "field-options": { + "cache-type": "ranked", + "cache-size": 100000 + } + } + ] + } + ` + + IPTests := []struct { + TestName string + ClientIP string + StatusCode int + }{ + {TestName: "ValidIP", ClientIP: validIP, StatusCode: http.StatusOK}, + {TestName: "InvalidIP", ClientIP: "10.0.1.1", StatusCode: http.StatusForbidden}, + } + + tests := []struct { + testName string + method string + url string + body string + }{ + { + testName: "Post-Index", + method: "POST", + url: fmt.Sprintf("%s/index/%s", m.URL(), index), + body: "", + }, + { + testName: "Post-field", + method: "POST", + url: fmt.Sprintf("%s/index/%s/field/%s", m.URL(), index, field), + body: "", + }, + { + testName: "Get-Schema", + method: "GET", + url: fmt.Sprintf("%s/schema", m.URL()), + body: "", + }, + { + testName: "Post-Schema", + method: "POST", + url: fmt.Sprintf("%s/internal/schema", m.URL()), + body: schema, + }, + { + testName: "Get-Shards", + method: "GET", + url: fmt.Sprintf("%s/internal/index/%s/shards", m.URL(), keyedIndex), + body: "", + }, + { + testName: "Post-CreateKeys", + method: "POST", + url: fmt.Sprintf("%s/internal/translate/index/%s/keys/create", m.URL(), keyedIndex), + body: names, + }, + { + testName: "Get-MemoryUsage", + method: "GET", + url: fmt.Sprintf("%s/internal/mem-usage", m.URL()), + body: "", + }, + { + testName: "Get-Status", + method: "GET", + url: fmt.Sprintf("%s/status", m.URL()), + body: "", + }, + } + + for _, ipTest := range IPTests { + for _, test := range tests { + t.Run(ipTest.TestName+"-"+test.testName, func(t *testing.T) { + var req *gohttp.Request + if test.body != "" { + req, err = gohttp.NewRequest(test.method, test.url, strings.NewReader(test.body)) + if err != nil { + t.Fatal(err) + } + } else { + req, err = gohttp.NewRequest(test.method, test.url, nil) + if err != nil { + t.Fatal(err) + } + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Forwarded-For", ipTest.ClientIP) + resp, err := gohttp.DefaultClient.Do(req) + if err != nil { + t.Fatalf("failed to send request: %v", err) + } + if resp.StatusCode != ipTest.StatusCode { + t.Fatalf("expected %v response code, got %v, body: %v", ipTest.StatusCode, resp.StatusCode, resp.Body) + } + + if ipTest.StatusCode == 200 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading resp body :%v", err) + } + + if test.testName == "Post-CreateKeys" { + var results map[string]uint64 + err = json.Unmarshal([]byte(body), &results) + if err != nil { + t.Fatalf("unmarshalling result: %v", err) + } + if len(results) != 3 { + t.Fatalf("finding keys before any were set: expected 3 results, got %d (%q)", len(results), results) + } + } + } + }) + } + } +} diff --git a/internal/clustertests/cluster_test.go b/internal/clustertests/cluster_test.go index 5c69299ff..604161cc4 100644 --- a/internal/clustertests/cluster_test.go +++ b/internal/clustertests/cluster_test.go @@ -64,6 +64,7 @@ func GetAuthToken(t *testing.T) string { ClientID, ClientSecret, Key, + []string{}, ) if err != nil { t.Fatalf("NewAuth: %v", err) diff --git a/internal_client.go b/internal_client.go index a4396282c..c4431de64 100644 --- a/internal_client.go +++ b/internal_client.go @@ -154,6 +154,12 @@ func AddAuthToken(ctx context.Context, req *http.Request) *http.Request { // UserInfo.Token is not prefixed with "Bearer" req.Header.Set("Authorization", "Bearer "+uinfo.(*authn.UserInfo).Token) } + + // set ogIP to request for remote calls + if ogIP, ok := ctx.Value(OriginalIPHeader).(string); ok && ogIP != "" { + req.Header.Set(OriginalIPHeader, ogIP) + } + return req } diff --git a/internal_client_test.go b/internal_client_test.go index 73fe9f07a..0eaae9ce4 100644 --- a/internal_client_test.go +++ b/internal_client_test.go @@ -1603,4 +1603,15 @@ func TestAddAuthToken(t *testing.T) { t.Fatalf("got '%v', expected '%v'", got, tok) } }) + t.Run("originalIP", func(t *testing.T) { + req, err := gohttp.NewRequest("GET", "dontmatternone", strings.NewReader("this doesn't matter")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + ogIP := "10.0.0.1" + pilosa.AddAuthToken(context.WithValue(context.Background(), pilosa.OriginalIPHeader, ogIP), req) + if got := req.Header.Get(pilosa.OriginalIPHeader); got != ogIP { + t.Fatalf("got '%v', expected '%v'", got, ogIP) + } + }) } diff --git a/qa/scripts/auth-smoke/tests/sup218-test.sh b/qa/scripts/auth-smoke/tests/sup218-test.sh index 0a9cd011e..d09a0fce4 100755 --- a/qa/scripts/auth-smoke/tests/sup218-test.sh +++ b/qa/scripts/auth-smoke/tests/sup218-test.sh @@ -16,9 +16,8 @@ done HOST=${HOSTS[2]} -ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4ifQ.I1iCgk1VU7m6e-En4ACTHIs6V2dZpy_8j2blSSo7K3U" # ingest string key data to user index -/data/datagen --source custom --custom-config /data/tests/sup218_datagen.yaml --pilosa.index=user --pilosa.hosts=https://$HOST:10101 --pilosa.batch-size=1000 --auth-token=$ADMIN_TOKEN +/data/datagen --source custom --custom-config /data/tests/sup218_datagen.yaml --pilosa.index=user --pilosa.hosts=https://$HOST:10101 --pilosa.batch-size=1000 ifErr "running datagen on $HOST" # install grpcurl diff --git a/qa/scripts/utilCluster.sh b/qa/scripts/utilCluster.sh index 062cc1e75..90956b3b3 100755 --- a/qa/scripts/utilCluster.sh +++ b/qa/scripts/utilCluster.sh @@ -128,6 +128,7 @@ long-query-time = "10s" secret-key = "98995f0530eeba96da1d0a04311073c0abb7b6abbfb0f5f4ef3629527ff88428" permissions = "/etc/permissions.yml" query-log-path = "/data/featurebase/query.log" + configured-ips = ["${DEPLOYED_INGEST_IPS}"] EOT echo "writing the permissions file" diff --git a/server/config.go b/server/config.go index 718ec0edb..caeb5314b 100644 --- a/server/config.go +++ b/server/config.go @@ -254,6 +254,7 @@ type Auth struct { SecretKey string `toml:"secret-key"` PermissionsFile string `toml:"permissions"` QueryLogPath string `toml:"query-log-path"` + ConfiguredIPs []string `toml:"configured-ips"` } // Namespace returns the namespace to use based on the Future flag. @@ -663,6 +664,39 @@ func (c *Config) ValidateAuth() (errors []error) { errors = append(errors, fmt.Errorf("must provide scope for authentication with IdP - for access and refresh token")) } + if len(c.Auth.ConfiguredIPs) > 0 { + for _, IP := range c.Auth.ConfiguredIPs { + if strings.Contains(IP, ":") { + errors = append(errors, fmt.Errorf("port is not allowed in IP %v for auth.configured-ips", IP)) + continue + } + if IP == "" { + errors = append(errors, fmt.Errorf("empty string for auth.configured-ips")) + continue + } + if strings.Contains(IP, "localhost") { + errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips, DNS names are not allowed", IP)) + continue + } + if strings.EqualFold(IP, "0.0.0.0") { + errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips", IP)) + continue + } + // validate CIDR addresses + if strings.Contains(IP, "/") { + if _, _, err := net.ParseCIDR(IP); err != nil { + errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips: %v", IP, err)) + continue + } + continue + } + // validate IP + if net.ParseIP(IP) == nil { + errors = append(errors, fmt.Errorf("%v is not a valid IP for auth.configured-ips", IP)) + continue + } + } + } return errors } diff --git a/server/config_internal_test.go b/server/config_internal_test.go index 1c377c4a1..2dd044497 100644 --- a/server/config_internal_test.go +++ b/server/config_internal_test.go @@ -281,6 +281,8 @@ func TestConfig_validateAuth(t *testing.T) { errorMesgURL := "invalid URL" errorMesgScope := "must provide scope" errorMesgKey := "invalid key length" + errorMesgIP := "not a valid IP for auth.configured-ips" + errorMesgPort := "port is not allowed in IP" validTestURL := "https://url.com/" validClientID := "clientid" validClientSecret := "clientSecret" @@ -290,6 +292,10 @@ func TestConfig_validateAuth(t *testing.T) { validStringSlice := []string{"https://graph.microsoft.com/.default", "offline_access"} validString := "asdfqwer1234asdfzxcv" var emptySlice []string + validIPList := []string{"10.0.0.1", "10.0.0.0", "10.0.0.0/32"} + invalidIPList := []string{"0.0.0.0", "localhost", "100.0.1", "100.0.0/32"} + emptyStringIP := []string{""} + portIP := []string{"10.0.0.1:10101"} enable := true disable := false @@ -321,6 +327,7 @@ func TestConfig_validateAuth(t *testing.T) { LogoutURL: emptyString, Scopes: validStringSlice, SecretKey: emptyString, + ConfiguredIPs: emptySlice, }, }, { @@ -339,6 +346,7 @@ func TestConfig_validateAuth(t *testing.T) { LogoutURL: validTestURL, Scopes: validStringSlice, SecretKey: validString, + ConfiguredIPs: validIPList, }, }, { @@ -359,6 +367,7 @@ func TestConfig_validateAuth(t *testing.T) { LogoutURL: invalidURL, Scopes: validStringSlice, SecretKey: validKey, + ConfiguredIPs: validIPList, }, }, { @@ -377,6 +386,7 @@ func TestConfig_validateAuth(t *testing.T) { LogoutURL: validTestURL, Scopes: emptySlice, SecretKey: validKey, + ConfiguredIPs: validIPList, }, }, { @@ -394,6 +404,7 @@ func TestConfig_validateAuth(t *testing.T) { Scopes: validStringSlice, SecretKey: validKey, QueryLogPath: "thisIsAPAth", + ConfiguredIPs: validIPList, }, }, { @@ -410,6 +421,67 @@ func TestConfig_validateAuth(t *testing.T) { LogoutURL: validTestURL, Scopes: validStringSlice, SecretKey: emptyString, + ConfiguredIPs: emptySlice, + }, + }, + { + // Auth enabled, all configs are set properly except configuredIPs + []string{ + errorMesgIP, + errorMesgIP, + errorMesgIP, + errorMesgIP, + }, + Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: validTestURL, + GroupEndpointURL: validTestURL, + RedirectBaseURL: validTestURL, + LogoutURL: validTestURL, + Scopes: validStringSlice, + SecretKey: validKey, + ConfiguredIPs: invalidIPList, + }, + }, + { + // Auth enabled, all configs are set properly except configuredIPs + []string{ + errorMesgEmpty, + }, + Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: validTestURL, + GroupEndpointURL: validTestURL, + RedirectBaseURL: validTestURL, + LogoutURL: validTestURL, + Scopes: validStringSlice, + SecretKey: validKey, + ConfiguredIPs: emptyStringIP, + }, + }, + { + // Auth enabled, all configs are set properly except configuredIPs + []string{ + errorMesgPort, + }, + Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: validTestURL, + GroupEndpointURL: validTestURL, + RedirectBaseURL: validTestURL, + LogoutURL: validTestURL, + Scopes: validStringSlice, + SecretKey: validKey, + ConfiguredIPs: portIP, }, }, } diff --git a/server/server.go b/server/server.go index db75c491b..d16e683f3 100644 --- a/server/server.go +++ b/server/server.go @@ -531,7 +531,7 @@ func (m *Command) SetupServer() error { } ac := m.Config.Auth - m.auth, err = authn.NewAuth(m.logger, ac.RedirectBaseURL, ac.Scopes, ac.AuthorizeURL, ac.TokenURL, ac.GroupEndpointURL, ac.LogoutURL, ac.ClientId, ac.ClientSecret, ac.SecretKey) + m.auth, err = authn.NewAuth(m.logger, ac.RedirectBaseURL, ac.Scopes, ac.AuthorizeURL, ac.TokenURL, ac.GroupEndpointURL, ac.LogoutURL, ac.ClientId, ac.ClientSecret, ac.SecretKey, ac.ConfiguredIPs) if err != nil { return errors.Wrap(err, "instantiating authN object") } @@ -544,6 +544,9 @@ func (m *Command) SetupServer() error { m.queryLogger.Infof("Starting Featurebase...") m.queryLogger.Infof("Group with admin level access: %v", p.Admin) m.queryLogger.Infof("Permissions: %+v", p.Permissions) + if len(ac.ConfiguredIPs) > 0 { + m.queryLogger.Infof("Configured IPs for allowed networks: %v", ac.ConfiguredIPs) + } // disable postgres binding if auth is enabled m.Config.Postgres.Bind = "" diff --git a/testdata/certs/localhost.crt b/testdata/certs/localhost.crt new file mode 100644 index 000000000..52d4c59ba --- /dev/null +++ b/testdata/certs/localhost.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEcDCCAtigAwIBAgIQUcwmfB3r1kcWpRm+pJCNQTANBgkqhkiG9w0BAQsFADCB +mTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTcwNQYDVQQLDC5zb3Vo +YWlsYW5vb3JAc291aGFpbGFzLW1icC5sYW4gKFNvdWhhaWxhIE5vb3IpMT4wPAYD +VQQDDDVta2NlcnQgc291aGFpbGFub29yQHNvdWhhaWxhcy1tYnAubGFuIChTb3Vo +YWlsYSBOb29yKTAeFw0yMjAyMDMxNzAxNTRaFw0yNDA1MDMxNjAxNTRaMGIxJzAl +BgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE3MDUGA1UECwwu +c291aGFpbGFub29yQHNvdWhhaWxhcy1tYnAubGFuIChTb3VoYWlsYSBOb29yKTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKlW+aTutSSDraStNa3T55eH +qR0F1Fxap7Zefd3TQuhNfZc7x9pyT8Rfd8n1fiL+NVAsrGWCX4kqrq5J3comPEZG +zphoWBlhko9jWWa375xTEp4Bo8lQQ1Z0GgucVk8XkmPPO7z6ZuMb2+LBtY1IkXcP +1nr0m/F1isQw2R5V51eBuNv44h0+CN+f706u5zsBrY4+wXS3l/hXzTEYAN/qryEi +xjTsvlURnZHEKNtC1QPOGJMe/Viqv3HW9vLs9r5b9+9R7mt3nSWXLHJWe5e8P3xY +s1aRWWWyd4F6q+0NYdlGN9SjN3MjaVuXCL76vvGaWmRAlYXm7y/+fIB8OIS/aV8C +AwEAAaNqMGgwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8G +A1UdIwQYMBaAFJzPC26j5NzKZj55kwlZB9ucEdk8MCAGA1UdEQQZMBeCCWxvY2Fs +aG9zdIcEAAAAAIcEwKhWLzANBgkqhkiG9w0BAQsFAAOCAYEAdTR/I1ONyQe/KKYh +R5pEWnAS7gGIW7EHkNoDhm/fv2+yplM7jpI7F6zBFIKIchSEWVSndnEerZ8lTcgG +27fuvmR4B0EpLQ3CK5jWNMMDEgSpX5pRMn6ZOuhRDglt0/+knsxra3rbUORfrl+w +FNAB7e1Q/q4eKVqxAiW0wpQEBnw3ko2/r2t1/x1diReDlb7NFztfwAxXyQKQbQvY +/V6r514cm7s4qqnEudaK8LR8G5wrTBJgRdwzkpiBsi0Kekibp2FUQcRQeUvPqgvn +c2VmJGVlWNJj5fhQO3Tn3NGLECA6jJFWVCMBHNP/DJtZjJQQhXyn4WC5r0g/aFwm +iDX2djhj43GyMT7V1fTirJpWQC4zOMIV54nZ90BULJD76+hmLrBccdZPcJHhpPw/ +WGvhV+IEjxLvr/RPgcZitFwYKKEMgQcGRl6oLo71hD99cEQXhM5bgv7uvYvX4XqG +W1S7Qst6xEkZ5nLMv0dLpd7SRCUXuIyNBUMkuAW7c2NY21lC +-----END CERTIFICATE----- diff --git a/testdata/certs/localhost.key b/testdata/certs/localhost.key new file mode 100644 index 000000000..dd828764a --- /dev/null +++ b/testdata/certs/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpVvmk7rUkg62k +rTWt0+eXh6kdBdRcWqe2Xn3d00LoTX2XO8fack/EX3fJ9X4i/jVQLKxlgl+JKq6u +Sd3KJjxGRs6YaFgZYZKPY1lmt++cUxKeAaPJUENWdBoLnFZPF5Jjzzu8+mbjG9vi +wbWNSJF3D9Z69JvxdYrEMNkeVedXgbjb+OIdPgjfn+9Oruc7Aa2OPsF0t5f4V80x +GADf6q8hIsY07L5VEZ2RxCjbQtUDzhiTHv1Yqr9x1vby7Pa+W/fvUe5rd50llyxy +VnuXvD98WLNWkVllsneBeqvtDWHZRjfUozdzI2lblwi++r7xmlpkQJWF5u8v/nyA +fDiEv2lfAgMBAAECggEAWxRFp5kgYqhUT9s9oOX1jUHRCqQlCRtVUzhVeGb1eJ6d +KydjIbIUBynV8xEQK+AQarPscZiCq/MCCwn9uYmBJ8dTvRN6NwSM4IRuBFpwDON9 +gvCc2F1MKoa9r3sBzP5/pSFEHyuLBSHpzXekn33lh2VEVdOUwHUZEta3IKLRj/V8 +eFNGLFGaRkTuVnjJpCySy89ANUxH7wXyz9g7S6WIvU0Gri6hh5JV5DZ0BYQWqrXs +obipibRiTtSjBbV3uRABlA6le8ffWR9VsiS1njNRiXDvTl/zZKQjOCQxYJ4hbKHK +BYZuz5mXxQSmZoLkFd3ttIQ98zGJGVR5HI20kt97UQKBgQDHPlB/38QUiLSRLWc0 +dDziXIAlk5X0IkYDdSj0mMi5OaSOVjtCCtzQO8ncuFDJv/bjhFCrNsaLhDrSMP9w +q+X5C6IyFfZoxhFm0Yjf87szJ4Gj3LKwL33y62+4a+FoL8e2rKU2BNhC/hs+hLv+ +GIg0WPUD6ApQqEzZiEy9sPwrlwKBgQDZk/TtmSO2vSsV4sUSdFYiKU/0ngsFw1yh +aAOQV5km3rqQP0bOmHvVmp8ZWWg0mRecEsih344kB1s3g60m6dfTYsfX+PCn4LoQ +bB/b3VfJmq5mHeicydI9vpggHOo4HXRvDFjuXcDuq8WCflR+RfWPDRMtr+fnOtYz +XzT/mv2JeQKBgDOUWDaicRp3wXcL7/nOVaysEaioqltHPCTNActAekYpPAZ1IGYa +dcuajsmLFa8E+R8xM8j/JysbEjcz5A0BE0oDzvt0YBQDoqGhgPOpHz9A7PjEu6WM +xehLNuLhWrskE2mhDCwN2QaOzfLDXf+Lzkbu+I8IVRNIXoy6ElPRK5cFAoGAUcy1 +dapwaeB+1VCXZNPGGjvL6pyMalNDfQ8838R/OMTeASM4/K0JSYpDLGWXrsarwKqz +MWB58QOvAJUJwSqDUN6/YWwfFg6ABXKRG7kAqzPzQ0MVU/TwjPQd/1y/le9E+gb7 +XEp1IYYE0IWsQHkO1ARHZJLQrfdJ1rCBnWNo1IkCgYEAtqgGtOgYb169TjftqDZD +7DdFUI6sweRfeRs91OGbLRpHU7LGia6GSm+CA8newUMJQtgT8EOR+OyDJBZTL1CS +dkk0bmW3Z+C0zBSKhNlEKE5Jg6rDJ2fpC87lWN+6ZbNOZbjiMkV4gPlVFy8fkSCn +5fSf7UT9/FFCrqKPFHPThXE= +-----END PRIVATE KEY-----