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

Add application-level per-realm firewall configuration #644

Merged
merged 2 commits into from
Sep 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
<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
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
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)
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
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