diff --git a/server/bindata.go b/server/bindata.go
index 716f561c52ed59dd775c7207b67feeef8002ef80..ede564c5002e70b57b5c8cc78ca495b3cc3fbdcf 100644
--- a/server/bindata.go
+++ b/server/bindata.go
@@ -1118,7 +1118,7 @@ var _templatesLogin_otpHtml = []byte(`{{template "header" .}}
       {{end}}
 
       <label for="inputOTP" class="sr-only">OTP</label>
-      <input type="text" id="inputOTP" name="otp" size="6" class="form-control" required autofocus>
+      <input type="text" id="inputOTP" name="otp" size="6" class="form-control" required autofocus autocomplete="off">
 
       <button type="submit" class="btn btn-primary">Login</button>
 
@@ -1137,7 +1137,7 @@ func templatesLogin_otpHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_otp.html", size: 543, mode: os.FileMode(420), modTime: time.Unix(1541234791, 0)}
+	info := bindataFileInfo{name: "templates/login_otp.html", size: 562, mode: os.FileMode(420), modTime: time.Unix(1550307595, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1164,13 +1164,13 @@ var _templatesLogin_passwordHtml = []byte(`{{template "header" .}}
              value="{{.Username}}"
              {{else}}
              placeholder="Username"
-             {{end}} required autofocus>
+             {{end}} autocomplete="off" required autofocus>
 
       <label for="inputPassword" class="sr-only">
         Password
       </label>
       <input type="password" name="password" id="inputPassword"
-             class="form-control" placeholder="Password" required>
+             class="form-control" placeholder="Password" autocomplete="off" required>
 
       {{if .AccountRecoveryURL}}
       <p>
@@ -1199,7 +1199,7 @@ func templatesLogin_passwordHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/login_password.html", size: 1149, mode: os.FileMode(420), modTime: time.Unix(1542882702, 0)}
+	info := bindataFileInfo{name: "templates/login_password.html", size: 1187, mode: os.FileMode(420), modTime: time.Unix(1550307045, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -1312,7 +1312,8 @@ var _templatesPageHtml = []byte(`{{define "header"}}<!DOCTYPE html>
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
     <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
     <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
-    <title>Sign In</title>
+    {{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
+    <title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
   </head>
 
   <body>
@@ -1347,7 +1348,7 @@ func templatesPageHtml() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "templates/page.html", size: 1729, mode: os.FileMode(420), modTime: time.Unix(1550305837, 0)}
+	info := bindataFileInfo{name: "templates/page.html", size: 1870, mode: os.FileMode(420), modTime: time.Unix(1550311459, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
diff --git a/server/config.go b/server/config.go
index 06f251e05b1a20ac87bc63b93858b5a50b6c86e9..b67de8837c6e63446829c1f13bb4307d1c04a885 100644
--- a/server/config.go
+++ b/server/config.go
@@ -42,6 +42,10 @@ type Config struct {
 	KeyStoreEnableGroups       []string                  `yaml:"keystore_enable_groups"`
 	AccountRecoveryURL         string                    `yaml:"account_recovery_url"`
 
+	SiteName    string `yaml:"site_name"`
+	SiteLogo    string `yaml:"site_logo"`
+	SiteFavicon string `yaml:"site_favicon"`
+
 	allowedServicesRx []*regexp.Regexp
 }
 
diff --git a/server/http.go b/server/http.go
index 12ecae6ef0e0d959bd98dc3955a63a1ef8dcd4cd..0fef31dc51ad95e2c46c3acb68fa729579130fa1 100644
--- a/server/http.go
+++ b/server/http.go
@@ -4,15 +4,18 @@ package server
 //go:generate go-bindata --nocompress --pkg server static/... templates/...
 
 import (
+	"bytes"
 	"context"
 	"encoding/gob"
 	"encoding/json"
 	"fmt"
 	"html/template"
 	"io"
+	"io/ioutil"
 	"log"
 	"net/http"
 	"net/url"
+	"os"
 	"strings"
 	"time"
 
@@ -91,9 +94,13 @@ type Server struct {
 	keystore            ksclient.Client
 	keystoreGroups      []string
 	csrfSecret          []byte
-	tpl                 *template.Template
+	renderer            *renderer
 	urlPrefix           string
 	homepageRedirectURL string
+
+	// User-configurable static data that we serve from memory.
+	siteLogo    *staticContent
+	siteFavicon *staticContent
 }
 
 func sl2bl(sl []string) [][]byte {
@@ -116,13 +123,14 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
 		Path:     urlPrefix + "/",
 	}
 
+	renderer := newRenderer(config)
 	s := &Server{
 		authSessionLifetime: defaultAuthSessionLifetime,
 		authSessionStore:    store,
 		loginService:        loginService,
 		urlPrefix:           urlPrefix,
 		homepageRedirectURL: config.HomepageRedirectURL,
-		tpl:                 parseEmbeddedTemplates(),
+		renderer:            renderer,
 	}
 	if config.CSRFSecret != "" {
 		s.csrfSecret = []byte(config.CSRFSecret)
@@ -131,6 +139,21 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
 		s.authSessionLifetime = time.Duration(config.AuthSessionLifetimeSeconds) * time.Second
 	}
 
+	if config.SiteLogo != "" {
+		siteLogo, err := loadStaticContent(config.SiteLogo)
+		if err != nil {
+			return nil, err
+		}
+		s.siteLogo = siteLogo
+	}
+	if config.SiteFavicon != "" {
+		siteFavicon, err := loadStaticContent(config.SiteFavicon)
+		if err != nil {
+			return nil, err
+		}
+		s.siteFavicon = siteFavicon
+	}
+
 	if config.KeyStore != nil {
 		ks, err := ksclient.New(config.KeyStore)
 		if err != nil {
@@ -146,8 +169,8 @@ func New(loginService *LoginService, authClient authclient.Client, config *Confi
 		return nil, err
 	}
 	s.loginHandler = newLoginHandler(s.loginCallback, devMgr, authClient,
-		config.AuthService, config.U2FAppID, config.URLPrefix, config.AccountRecoveryURL,
-		s.tpl, sessionSecrets...)
+		config.AuthService, config.U2FAppID, config.URLPrefix,
+		renderer, sessionSecrets...)
 
 	return s, nil
 }
@@ -309,11 +332,9 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
 
 	svcJSON, _ := json.Marshal(svcs) // nolint
 	data := map[string]interface{}{
-		"CSRFField":            csrf.TemplateField(req),
-		"URLPrefix":            h.urlPrefix,
 		"Services":             svcs,
-		"IncludeLogoutScripts": true,
 		"ServicesJSON":         string(svcJSON),
+		"IncludeLogoutScripts": true,
 	}
 
 	// Clear the local session. Ignore errors.
@@ -335,7 +356,13 @@ func (h *Server) handleLogout(w http.ResponseWriter, req *http.Request, session
 
 	w.Header().Set("Content-Security-Policy", logoutContentSecurityPolicy)
 
-	h.tpl.ExecuteTemplate(w, "logout.html", data) // nolint
+	body, err := h.renderer.Render(req, "logout.html", data)
+	if err != nil {
+		log.Printf("template error in logout(): %v", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.Write(body) // nolint
 }
 
 func (h *Server) handleExchange(w http.ResponseWriter, req *http.Request) {
@@ -376,6 +403,14 @@ func (h *Server) Handler() http.Handler {
 	// everywhere.
 	root := mux.NewRouter()
 
+	// If we have customized content, serve it from well-known URLs.
+	if h.siteLogo != nil {
+		root.Handle(h.urlFor("/img/site_logo"), h.siteLogo)
+	}
+	if h.siteFavicon != nil {
+		root.Handle(h.urlFor("/favicon.ico"), h.siteFavicon)
+	}
+
 	// Serve static content to anyone.
 	staticPath := h.urlFor("/static/")
 	root.PathPrefix(staticPath).Handler(http.StripPrefix(staticPath, http.FileServer(&assetfs.AssetFS{
@@ -458,3 +493,65 @@ func parseEmbeddedTemplates() *template.Template {
 	}
 	return root
 }
+
+type renderer struct {
+	tpl *template.Template
+
+	urlPrefix          string
+	siteName           string
+	siteLogo           string
+	siteFavicon        string
+	accountRecoveryURL string
+}
+
+func newRenderer(config *Config) *renderer {
+	return &renderer{
+		tpl:                parseEmbeddedTemplates(),
+		urlPrefix:          config.URLPrefix,
+		accountRecoveryURL: config.AccountRecoveryURL,
+		siteName:           config.SiteName,
+		siteLogo:           config.SiteLogo,
+		siteFavicon:        config.SiteFavicon,
+	}
+}
+
+func (r *renderer) Render(req *http.Request, templateName string, data map[string]interface{}) ([]byte, error) {
+	data["CSRFField"] = csrf.TemplateField(req)
+	data["URLPrefix"] = r.urlPrefix
+	data["AccountRecoveryURL"] = r.accountRecoveryURL
+	data["SiteName"] = r.siteName
+	data["SiteLogo"] = r.siteLogo
+	data["SiteFavicon"] = r.siteFavicon
+
+	var buf bytes.Buffer
+	if err := r.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+type staticContent struct {
+	modtime time.Time
+	name    string
+	data    []byte
+}
+
+func loadStaticContent(path string) (*staticContent, error) {
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	data, err := ioutil.ReadFile(path) // #nosec
+	if err != nil {
+		return nil, err
+	}
+	return &staticContent{
+		name:    path,
+		modtime: stat.ModTime(),
+		data:    data,
+	}, nil
+}
+
+func (c *staticContent) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	http.ServeContent(w, req, c.name, c.modtime, bytes.NewReader(c.data))
+}
diff --git a/server/login.go b/server/login.go
index 3118f933bbc1647b749a2fa62986ff273fddb3c8..cc7f5f021cbab3d4d0e4818d0ce3cbd59dea259c 100644
--- a/server/login.go
+++ b/server/login.go
@@ -1,19 +1,16 @@
 package server
 
 import (
-	"bytes"
 	"encoding/gob"
 	"encoding/json"
 	"errors"
 	"fmt"
-	"html/template"
 	"log"
 	"net/http"
 	"net/url"
 	"strings"
 	"time"
 
-	"github.com/gorilla/csrf"
 	"github.com/gorilla/sessions"
 	"github.com/tstranex/u2f"
 	"go.opencensus.io/trace"
@@ -72,20 +69,19 @@ func init() {
 type loginCallbackFunc func(http.ResponseWriter, *http.Request, string, string, *auth.UserInfo) error
 
 type loginHandler struct {
-	authClient         authclient.Client
-	authService        string
-	u2fAppID           string
-	urlPrefix          string
-	devMgr             *device.Manager
-	loginCallback      loginCallbackFunc
-	loginSessionStore  sessions.Store
-	tpl                *template.Template
-	accountRecoveryURL string
+	authClient        authclient.Client
+	authService       string
+	u2fAppID          string
+	urlPrefix         string
+	devMgr            *device.Manager
+	loginCallback     loginCallbackFunc
+	loginSessionStore sessions.Store
+	renderer          *renderer
 }
 
 // NewLoginHandler will wrap an http.Handler with the login workflow,
 // invoking it only on successful login.
-func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix, accountRecoveryURL string, tpl *template.Template, keyPairs ...[]byte) *loginHandler {
+func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authClient authclient.Client, authService, u2fAppID, urlPrefix string, rndr *renderer, keyPairs ...[]byte) *loginHandler {
 	store := sessions.NewCookieStore(keyPairs...)
 	store.Options = &sessions.Options{
 		HttpOnly: true,
@@ -93,15 +89,14 @@ func newLoginHandler(okHandler loginCallbackFunc, devMgr *device.Manager, authCl
 		MaxAge:   0,
 	}
 	return &loginHandler{
-		authClient:         authClient,
-		authService:        authService,
-		u2fAppID:           u2fAppID,
-		urlPrefix:          strings.TrimRight(urlPrefix, "/"),
-		devMgr:             devMgr,
-		loginCallback:      okHandler,
-		loginSessionStore:  store,
-		accountRecoveryURL: accountRecoveryURL,
-		tpl:                parseEmbeddedTemplates(),
+		authClient:        authClient,
+		authService:       authService,
+		u2fAppID:          u2fAppID,
+		urlPrefix:         strings.TrimRight(urlPrefix, "/"),
+		devMgr:            devMgr,
+		loginCallback:     okHandler,
+		loginSessionStore: store,
+		renderer:          rndr,
 	}
 }
 
@@ -245,7 +240,8 @@ func (l *loginHandler) handlePassword(w http.ResponseWriter, req *http.Request,
 		env["Error"] = true
 	}
 
-	return l.executeTemplateToBuffer(req, "login_password.html", env)
+	body, err := l.renderer.Render(req, "login_password.html", env)
+	return loginStateNone, body, err
 }
 
 // Handle login with password and TOTP.
@@ -265,7 +261,8 @@ func (l *loginHandler) handleOTP(w http.ResponseWriter, req *http.Request, sessi
 		env["Error"] = true
 	}
 
-	return l.executeTemplateToBuffer(req, "login_otp.html", env)
+	body, err := l.renderer.Render(req, "login_otp.html", env)
+	return loginStateNone, body, err
 }
 
 // Handle login with password and hardware token.
@@ -293,7 +290,8 @@ func (l *loginHandler) handleU2F(w http.ResponseWriter, req *http.Request, sessi
 		env["Error"] = true
 	}
 
-	return l.executeTemplateToBuffer(req, "login_u2f.html", env)
+	body, err := l.renderer.Render(req, "login_u2f.html", env)
+	return loginStateNone, body, err
 }
 
 // Make the auth request to the authentication server.
@@ -346,19 +344,6 @@ func (l *loginHandler) makeLoginURL(req *http.Request) string {
 	return fmt.Sprintf("%s/login?%s", l.urlPrefix, v.Encode())
 }
 
-// Renders a template to a buffer, with a return value that makes it
-// convenient to use in login handlers.
-func (l *loginHandler) executeTemplateToBuffer(req *http.Request, templateName string, data map[string]interface{}) (loginState, []byte, error) {
-	data["CSRFField"] = csrf.TemplateField(req)
-	data["URLPrefix"] = l.urlPrefix
-	data["AccountRecoveryURL"] = l.accountRecoveryURL
-	var buf bytes.Buffer
-	if err := l.tpl.ExecuteTemplate(&buf, templateName, data); err != nil {
-		return loginStateNone, nil, err
-	}
-	return loginStateNone, buf.Bytes(), nil
-}
-
 // Template helper function that encodes its input as JSON.
 func toJSON(obj interface{}) string {
 	data, err := json.Marshal(obj)
diff --git a/server/templates/page.html b/server/templates/page.html
index a216a3f22a10646e0067265cfc23ee624229e967..8615364f735f721a1272091328aef692910d5a0d 100644
--- a/server/templates/page.html
+++ b/server/templates/page.html
@@ -6,7 +6,8 @@
     {{if .U2FSignRequest}}<meta name="u2f_request" value="{{json .U2FSignRequest}}">{{end}}
     <link rel="stylesheet" href="{{.URLPrefix}}/static/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO">
     <link rel="stylesheet" href="{{.URLPrefix}}/static/css/signin.css" integrity="sha384-9Y3UkAyM3svAuamEoaXIxe+1MqBKJdZtL8S1FZjvE1XqkICDH7DTXNavnFV8Uk2o">
-    <title>Sign In</title>
+    {{if .SiteFavicon}}<link rel="icon" type="image/x-icon" href="{{.URLPrefix}}/favicon.ico">{{end}}
+    <title>{{if .SiteName}}{{.SiteName}} - {{end}}Sign In</title>
   </head>
 
   <body>