Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Commit

Permalink
Add application-level per-realm firewall configuration (#644)
Browse files Browse the repository at this point in the history
* Add application-level per-realm firewall configuration

* Review comments
  • Loading branch information
sethvargo authored Sep 23, 2020
1 parent 2e4136b commit 78b7b9e
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 22 deletions.
13 changes: 7 additions & 6 deletions cmd/adminapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,17 @@ func realMain(ctx context.Context) error {
// first to reduce the chance of a database lookup.
r.Use(rateLimit)

// Other common middlewares
requireAPIKey := middleware.RequireAPIKey(ctx, cacher, db, h, []database.APIUserType{
database.APIUserTypeAdmin,
})
processFirewall := middleware.ProcessFirewall(ctx, h, "adminapi")

r.Handle("/health", controller.HandleHealthz(ctx, &config.Database, h)).Methods("GET")
{
sub := r.PathPrefix("/api").Subrouter()

// Setup API auth
requireAPIKey := middleware.RequireAPIKey(ctx, cacher, db, h, []database.APIUserType{
database.APIUserTypeAdmin,
})
// Install the APIKey Auth Middleware
sub.Use(requireAPIKey)
sub.Use(processFirewall)

issueapiController, err := issueapi.New(ctx, config, db, limiterStore, h)
if err != nil {
Expand Down
13 changes: 7 additions & 6 deletions cmd/apiserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,18 @@ func realMain(ctx context.Context) error {
// first to reduce the chance of a database lookup.
r.Use(rateLimit)

// Other common middlewares
requireAPIKey := middleware.RequireAPIKey(ctx, cacher, db, h, []database.APIUserType{
database.APIUserTypeDevice,
})
processFirewall := middleware.ProcessFirewall(ctx, h, "apiserver")

r.Handle("/health", controller.HandleHealthz(ctx, &config.Database, h)).Methods("GET")

{
sub := r.PathPrefix("/api").Subrouter()

// Setup API auth
requireAPIKey := middleware.RequireAPIKey(ctx, cacher, db, h, []database.APIUserType{
database.APIUserTypeDevice,
})
// Install the APIKey Auth Middleware
sub.Use(requireAPIKey)
sub.Use(processFirewall)

// POST /api/verify
verifyChaff := chaff.New()
Expand Down
54 changes: 54 additions & 0 deletions cmd/server/assets/realmadmin/_form_security.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<option value="0" {{if eq $realm.EmailVerifiedMode.String "prompt"}}selected{{end}}>Prompt after login</option>
<option value="2" {{if eq $realm.EmailVerifiedMode.String "optional"}}selected{{end}}>Optional</option>
</select>
{{template "errorable" $realm.ErrorsFor "emailVerifiedMode"}}
<small class="form-text text-muted">
Email verification requires users to verify their email address before
using the system.
Expand All @@ -30,6 +31,7 @@
<option value="0" {{if eq $realm.MFAMode.String "prompt"}}selected{{end}}>Prompt after login</option>
<option value="2" {{if eq $realm.MFAMode.String "optional"}}selected{{end}}>Optional</option>
</select>
{{template "errorable" $realm.ErrorsFor "mfaMode"}}
<small class="form-text text-muted">
Multi-factor authentication requires users to supply a second factor (e.g.
a code via an SMS text message) when authenticating to the system.
Expand All @@ -44,6 +46,7 @@
<option value="{{$prd}}" {{if (eq $prd $current)}}selected{{end}}>{{if (eq $prd 0)}}Off{{else}}Every {{$prd}} days{{end}}</option>
{{end}}
</select>
{{template "errorable" $realm.ErrorsFor "passwordRotationPeriodDays"}}
<small class="form-text text-muted">
If enabled, users will be required to change their password after this
number of days elapse since their last password change.
Expand All @@ -58,12 +61,63 @@
<option value="{{$pwd }}" {{if (eq $pwd $current)}}selected{{end}}>{{if (eq $pwd 0)}}Off{{else}}{{$pwd}} days before{{end}}</option>
{{end}}
</select>
{{template "errorable" $realm.ErrorsFor "passwordRotationWarningDays"}}
<small class="form-text text-muted">
If enabled, users will be warned to change their password within this
number of days before expiration.
</small>
</div>

<div class="form-label-group">
<textarea name="allowed_cidrs_adminapi" id="allowed-cidrs-adminapi" class="form-control text-monospace{{if $realm.ErrorsFor "allowedCIDRsAdminAPI"}} is-invalid{{end}}"
rows="5" placeholder="Allowed CIDRs (Admin API)">{{joinStrings $realm.AllowedCIDRsAdminAPI "\n"}}</textarea>
<label for="allowed-cidrs-adminapi">Allowed CIDRs (Admin API)</label>
{{template "errorable" $realm.ErrorsFor "allowedCIDRsAdminAPI"}}
<small class="form-text text-muted">
An optional list of CIDR blocks from which to allow traffic to the
<strong>Admin API</strong> which can be used to generate codes. If blank,
all traffic is allowed from all IPs. These should be of <a
href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing"
target="_BLANK">CIDR notation</a>
of the format (e.g. <code>192.1.2.0/24</code>). This is only enforced
post-authentication.
</small>
</div>

<div class="form-label-group">
<textarea name="allowed_cidrs_apiserver" id="allowed-cidrs-apiserver" class="form-control text-monospace{{if $realm.ErrorsFor "allowedCIDRsAPIServer"}} is-invalid{{end}}"
rows="5" placeholder="Allowed CIDRs (Device API)">{{joinStrings $realm.AllowedCIDRsAPIServer "\n"}}</textarea>
<label for="allowed-cidrs-apiserver">Allowed CIDRs (Device API)</label>
{{template "errorable" $realm.ErrorsFor "allowedCIDRsAPIServer"}}
<small class="form-text text-muted">
An optional list of CIDR blocks from which to allow traffic to the
<strong>Device API</strong> which is where devices exchange their code for
a certificate. It is highly recommended that you <strong>leave this
service publicly accessible</strong>. If blank, all traffic is allowed
from all IPs. These should be of <a
href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing"
target="_BLANK">CIDR notation</a>
of the format (e.g. <code>192.1.2.0/24</code>). This is only enforced
post-authentication.
</small>
</div>

<div class="form-label-group">
<textarea name="allowed_cidrs_server" id="allowed-cidrs-server" class="form-control text-monospace{{if $realm.ErrorsFor "allowedCIDRsServer"}} is-invalid{{end}}"
rows="5" placeholder="Allowed CIDRs (UI server)">{{joinStrings $realm.AllowedCIDRsServer "\n"}}</textarea>
<label for="allowed-cidrs-server">Allowed CIDRs (UI server)</label>
{{template "errorable" $realm.ErrorsFor "allowedCIDRsServer"}}
<small class="form-text text-muted">
An optional list of CIDR blocks from which to allow traffic to the
<strong>UI server</strong> (this server). If blank, all traffic is allowed
from all IPs. These should be of <a
href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing"
target="_BLANK">CIDR notation</a>
of the format (e.g. <code>192.1.2.0/24</code>). This is only enforced
post-authentication.
</small>
</div>

<div class="mt-4">
<input type="submit" class="btn btn-primary btn-block" value="Update security settings" />
</div>
Expand Down
11 changes: 11 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ func realMain(ctx context.Context) error {
requireRealm := middleware.RequireRealm(ctx, h)
requireSystemAdmin := middleware.RequireAdmin(ctx, h)
requireMFA := middleware.RequireMFA(ctx, h)
processFirewall := middleware.ProcessFirewall(ctx, h, "server")
rateLimit := httplimiter.Handle

{
Expand Down Expand Up @@ -242,6 +243,7 @@ func realMain(ctx context.Context) error {
sub.Use(rateLimit)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(processFirewall)
sub.Handle("/login/verify-email", loginController.HandleVerifyEmail()).Methods("GET")

// SMS auth registration is realm-specific, so it needs to load the current realm.
Expand All @@ -250,6 +252,7 @@ func realMain(ctx context.Context) error {
sub.Use(rateLimit)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(processFirewall)
sub.Use(requireVerified)
sub.Handle("/login/register-phone", loginController.HandleRegisterPhone()).Methods("GET")
}
Expand All @@ -260,6 +263,7 @@ func realMain(ctx context.Context) error {
sub.Use(requireAuth)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(processFirewall)
sub.Use(requireVerified)
sub.Use(requireMFA)
sub.Use(rateLimit)
Expand All @@ -280,6 +284,7 @@ func realMain(ctx context.Context) error {
sub.Use(requireAuth)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(processFirewall)
sub.Use(requireVerified)
sub.Use(requireMFA)
sub.Use(rateLimit)
Expand All @@ -295,6 +300,8 @@ func realMain(ctx context.Context) error {
sub := r.PathPrefix("/apikeys").Subrouter()
sub.Use(requireAuth)
sub.Use(loadCurrentRealm)
sub.Use(requireRealm)
sub.Use(processFirewall)
sub.Use(requireAdmin)
sub.Use(requireVerified)
sub.Use(requireMFA)
Expand All @@ -316,6 +323,8 @@ func realMain(ctx context.Context) error {
userSub := r.PathPrefix("/users").Subrouter()
userSub.Use(requireAuth)
userSub.Use(loadCurrentRealm)
userSub.Use(requireRealm)
userSub.Use(processFirewall)
userSub.Use(requireAdmin)
userSub.Use(requireVerified)
userSub.Use(requireMFA)
Expand All @@ -339,6 +348,8 @@ func realMain(ctx context.Context) error {
realmSub := r.PathPrefix("/realm").Subrouter()
realmSub.Use(requireAuth)
realmSub.Use(loadCurrentRealm)
realmSub.Use(requireRealm)
realmSub.Use(processFirewall)
realmSub.Use(requireAdmin)
realmSub.Use(requireVerified)
realmSub.Use(requireMFA)
Expand Down
22 changes: 20 additions & 2 deletions pkg/controller/middleware/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func RequireAPIKey(ctx context.Context, cacher cache.Cacher, db *database.Databa
// Load the authorized app by using the cache to alleviate pressure on the
// database layer.
var authApp database.AuthorizedApp
cacheKey := fmt.Sprintf("authorized_apps:by_api_key:%s", apiKey)
if err := cacher.Fetch(ctx, cacheKey, &authApp, cacheTTL, func() (interface{}, error) {
authAppCacheKey := fmt.Sprintf("authorized_apps:by_api_key:%s", apiKey)
if err := cacher.Fetch(ctx, authAppCacheKey, &authApp, cacheTTL, func() (interface{}, error) {
return db.FindAuthorizedAppByAPIKey(apiKey)
}); err != nil {
if database.IsNotFound(err) {
Expand All @@ -85,8 +85,26 @@ func RequireAPIKey(ctx context.Context, cacher cache.Cacher, db *database.Databa
return
}

// Lookup the realm.
var realm database.Realm
realmCacheKey := fmt.Sprintf("realms:by_id:%d", authApp.RealmID)
if err := cacher.Fetch(ctx, realmCacheKey, &realm, cacheTTL, func() (interface{}, error) {
return authApp.Realm(db)
}); err != nil {
if database.IsNotFound(err) {
logger.Warnw("realm does not exist", "id", authApp.RealmID)
controller.Unauthorized(w, r, h)
return
}

logger.Errorw("failed to lookup realm from authorized app", "error", err)
controller.InternalError(w, r, h, err)
return
}

// Save the authorized app on the context.
ctx = controller.WithAuthorizedApp(ctx, &authApp)
ctx = controller.WithRealm(ctx, &realm)
*r = *r.WithContext(ctx)

next.ServeHTTP(w, r)
Expand Down
101 changes: 101 additions & 0 deletions pkg/controller/middleware/firewall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package middleware

import (
"context"
"net"
"net/http"
"strings"

"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/render"

"github.com/google/exposure-notifications-server/pkg/logging"

"github.com/gorilla/mux"
)

// ProcessFirewall verifies the application-level firewall configuration.
//
// This must come after the realm has been loaded in the context, probably via a
// different middleware.
func ProcessFirewall(ctx context.Context, h *render.Renderer, typ string) mux.MiddlewareFunc {
logger := logging.FromContext(ctx).Named("middleware.ProcessFirewall")

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

realm := controller.RealmFromContext(ctx)
if realm == nil {
controller.MissingRealm(w, r, h)
return
}

var allowedCIDRs []string
switch typ {
case "adminapi":
allowedCIDRs = realm.AllowedCIDRsAdminAPI
case "apiserver":
allowedCIDRs = realm.AllowedCIDRsAPIServer
case "server":
allowedCIDRs = realm.AllowedCIDRsServer
default:
logger.Errorw("unknown firewall type", "type", typ)
}

// If there's no CIDRs, all traffic is allowed.
if len(allowedCIDRs) == 0 {
next.ServeHTTP(w, r)
return
}

logger.Debugw("validating ip in cidr block", "type", typ)

// Get the remote address.
ipStr := r.RemoteAddr

// Check if x-forwarded-for exists, the load balancer sets this, and the
// first entry is the real client IP.
xff := r.Header.Get("x-forwarded-for")
if xff != "" {
ipStr = strings.Split(xff, ",")[0]
}

// Parse as an IP.
ip := net.ParseIP(ipStr)
if ip == nil {
logger.Errorw("provided ip could not be parsed")
}

for _, c := range allowedCIDRs {
_, cidr, err := net.ParseCIDR(c)
if err != nil {
logger.Warnw("failed to parse cidr", "cidr", c, "error", err)
continue
}

if cidr.Contains(ip) {
next.ServeHTTP(w, r)
return
}
}

logger.Errorw("ip is not in an allowed cidr block")
controller.Unauthorized(w, r, h)
})
}
}
40 changes: 35 additions & 5 deletions pkg/controller/realmadmin/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,14 @@ func (c *Controller) HandleSettings() http.Handler {
TwilioAuthToken string `form:"twilio_auth_token"`
TwilioFromNumber string `form:"twilio_from_number"`

Security bool `form:"security"`
MFAMode int16 `form:"mfa_mode"`
EmailVerifiedMode int16 `form:"email_verified_mode"`
PasswordRotationPeriodDays uint `form:"password_rotation_period_days"`
PasswordRotationWarningDays uint `form:"password_rotation_warning_days"`
Security bool `form:"security"`
MFAMode int16 `form:"mfa_mode"`
EmailVerifiedMode int16 `form:"email_verified_mode"`
PasswordRotationPeriodDays uint `form:"password_rotation_period_days"`
PasswordRotationWarningDays uint `form:"password_rotation_warning_days"`
AllowedCIDRsAdminAPI string `form:"allowed_cidrs_adminapi"`
AllowedCIDRsAPIServer string `form:"allowed_cidrs_apiserver"`
AllowedCIDRsServer string `form:"allowed_cidrs_server"`

AbusePrevention bool `form:"abuse_prevention"`
AbusePreventionEnabled bool `form:"abuse_prevention_enabled"`
Expand Down Expand Up @@ -135,6 +138,33 @@ func (c *Controller) HandleSettings() http.Handler {
realm.MFAMode = database.AuthRequirement(form.MFAMode)
realm.PasswordRotationPeriodDays = form.PasswordRotationPeriodDays
realm.PasswordRotationWarningDays = form.PasswordRotationWarningDays

allowedCIDRsAdminADPI, err := database.ToCIDRList(form.AllowedCIDRsAdminAPI)
if err != nil {
realm.AddError("allowedCIDRsAdminAPI", err.Error())
flash.Error("Failed to update realm")
c.renderSettings(ctx, w, r, realm, nil)
return
}
realm.AllowedCIDRsAdminAPI = allowedCIDRsAdminADPI

allowedCIDRsAPIServer, err := database.ToCIDRList(form.AllowedCIDRsAPIServer)
if err != nil {
realm.AddError("allowedCIDRsAPIServer", err.Error())
flash.Error("Failed to update realm")
c.renderSettings(ctx, w, r, realm, nil)
return
}
realm.AllowedCIDRsAPIServer = allowedCIDRsAPIServer

allowedCIDRsServer, err := database.ToCIDRList(form.AllowedCIDRsServer)
if err != nil {
realm.AddError("allowedCIDRsServer", err.Error())
flash.Error("Failed to update realm")
c.renderSettings(ctx, w, r, realm, nil)
return
}
realm.AllowedCIDRsServer = allowedCIDRsServer
}

// Abuse prevention
Expand Down
Loading

0 comments on commit 78b7b9e

Please sign in to comment.