diff --git a/reviewboard/accounts/decorators.py b/reviewboard/accounts/decorators.py
index dbe12f142df3509f0e3f368bf3b2bd6df97a591a..c722ee4217feb1f7f4b182ea0cba4f3f7bfe20a8 100644
--- a/reviewboard/accounts/decorators.py
+++ b/reviewboard/accounts/decorators.py
@@ -1,10 +1,16 @@
 from __future__ import unicode_literals
 
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import \
+    login_required as django_login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
 from djblets.siteconfig.models import SiteConfiguration
 from djblets.util.decorators import simple_decorator
 
+from functools import wraps
+
 from reviewboard.accounts.models import Profile
+from reviewboard.admin.read_only import is_site_read_only_for
 
 
 @simple_decorator
@@ -20,12 +26,63 @@ def check_login_required(view_func):
         siteconfig = SiteConfiguration.objects.get_current()
 
         if siteconfig.get("auth_require_sitewide_login"):
-            return login_required(view_func)(*args, **kwargs)
+            return django_login_required(view_func)(*args, **kwargs)
         else:
             return view_func(*args, **kwargs)
 
     return _check
 
+def login_required(view_func=None, redirect_read_only=None):
+    """Provide appropriate decorator to check if user is logged in.
+
+    Some views need to be redirected to the login page if the user is not
+    logged in, which is done by django's login_required decorator. This
+    decorator wraps it with custom functionality for read-only mode.
+
+    If redirect_read_only is set, a decorator that handles read-only redirects
+    appropriately will be returned. Without anything set it will behave like
+    the original login_required decorator.
+
+    Args:
+        view_func (callable):
+            The view being called.
+
+        redirect_read_only (bool):
+            Boolean on whether a view should be redirected in read-only mode.
+
+    Returns:
+        callable:
+        The wrapped view function.
+    """
+    def _redirect_read_only_decorator(view):
+        @wraps(view)
+        def _login_with_read_only_check(request, *args, **kwargs):
+            if is_site_read_only_for(request.user):
+                return HttpResponseRedirect(reverse('503'))
+
+            return django_login_required(view)(request, *args, **kwargs)
+
+        return _login_with_read_only_check
+
+    def _without_redirect_read_only_decorator(view):
+        @wraps(view)
+        def _login_without_read_only_check(request, *args, **kwargs):
+            return django_login_required(view)(request, *args, **kwargs)
+
+        return _login_without_read_only_check
+
+    # If login_required is passed a view_func then it must not have
+    # redirect_read_only set.
+    if view_func:
+        return django_login_required(view_func)
+
+    # If redirect_read_only is set, then it is used without a view_func
+    # passed in, so return the appropriate decorator.
+    if redirect_read_only:
+        return _redirect_read_only_decorator
+    else:
+        return _without_redirect_read_only_decorator
+
 
 @simple_decorator
 def valid_prefs_required(view_func):
diff --git a/reviewboard/accounts/views.py b/reviewboard/accounts/views.py
index 445a6114b7accf6739ad0110d04110c78843aa2f..7650cebe25654925b954762b11781616a9db1791 100644
--- a/reviewboard/accounts/views.py
+++ b/reviewboard/accounts/views.py
@@ -1,6 +1,5 @@
 from __future__ import unicode_literals
 
-from django.contrib.auth.decorators import login_required
 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.http import Http404, HttpResponse, HttpResponseRedirect
@@ -16,6 +15,7 @@ from djblets.siteconfig.models import SiteConfiguration
 from djblets.util.decorators import augment_method_from
 
 from reviewboard.accounts.backends import get_enabled_auth_backends
+from reviewboard.accounts.decorators import login_required
 from reviewboard.accounts.forms.registration import RegistrationForm
 from reviewboard.accounts.pages import AccountPage
 from reviewboard.admin.server import build_server_url, get_server_url
@@ -33,7 +33,8 @@ def account_register(request, next_url='dashboard'):
     auth_backends = get_enabled_auth_backends()
 
     if (auth_backends[0].supports_registration and
-            siteconfig.get("auth_enable_registration")):
+        siteconfig.get('auth_enable_registration') and
+        not siteconfig.get('site_read_only')):
         response = register(request, next_page=reverse(next_url),
                             form_class=RegistrationForm)
 
@@ -62,7 +63,7 @@ class MyAccountView(ConfigPagesView):
         'account-page',
     ]
 
-    @method_decorator(login_required)
+    @method_decorator(login_required(redirect_read_only=True))
     @augment_method_from(ConfigPagesView)
     def dispatch(self, *args, **kwargs):
         """Handle the view.
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index 44ded2228218aa8c5c3cd244f3bd807301cf8a17..c43e0fd331f249919607e44e6a38ce3278fc008d 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -4,7 +4,6 @@ import logging
 import time
 
 from django.conf import settings
-from django.contrib.auth.decorators import login_required
 from django.contrib.auth.models import User
 from django.contrib.sites.models import Site
 from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
@@ -32,6 +31,7 @@ from djblets.util.http import (encode_etag, set_last_modified,
                                set_etag, etag_if_none_match)
 
 from reviewboard.accounts.decorators import (check_login_required,
+                                             login_required,
                                              valid_prefs_required)
 from reviewboard.accounts.models import ReviewRequestVisit, Profile
 from reviewboard.admin.read_only import is_site_read_only_for
@@ -274,7 +274,7 @@ def root(request, local_site_name=None):
         local_site_reverse(url_name, local_site_name=local_site_name))
 
 
-@login_required
+@login_required(redirect_read_only=True)
 @check_local_site_access
 def new_review_request(request,
                        local_site=None,
diff --git a/reviewboard/templates/503.html b/reviewboard/templates/503.html
new file mode 100644
index 0000000000000000000000000000000000000000..46c0e74d4812960a8a818eb8c4dc3d7b6417dae9
--- /dev/null
+++ b/reviewboard/templates/503.html
@@ -0,0 +1,16 @@
+{% extends "base.html" %}
+
+{% load djblets_deco i18n %}
+
+{% block title %}{% trans "Read-Only Mode" %}{% endblock %}
+
+{% block content %}
+{%  box "important" %}
+  <h1>{% trans "Read-Only Mode" %}</h1>
+  <p>
+{%   blocktrans %}
+   The site is currently in read-only mode and some features may be unavailable at the moment.
+{%   endblocktrans %}
+  </p>
+{%  endbox %}
+{% endblock %}
diff --git a/reviewboard/templates/accounts/login.html b/reviewboard/templates/accounts/login.html
index 5448de753e40389c58a4a3f41a676143362c861f..cbb66c00582119af01dd61c08513bf90cea71b83 100644
--- a/reviewboard/templates/accounts/login.html
+++ b/reviewboard/templates/accounts/login.html
@@ -42,7 +42,7 @@
 
  <div class="auth-form-row login-links">
 {%  if auth_backends.0.supports_registration %}
-{%   if siteconfig_settings.auth_enable_registration %}
+{%   if siteconfig_settings.auth_enable_registration and not siteconfig_settings.site_read_only %}
   <p>
    <a href="{% url 'register' %}">{% trans "Create an account" %}</a>
   </p>
diff --git a/reviewboard/templates/base/_nav_support_menu.html b/reviewboard/templates/base/_nav_support_menu.html
index 7812ca68e74608e634cc52b736aff3c278086018..81b05453eb6013535433c0f8887d3a0ae834965c 100644
--- a/reviewboard/templates/base/_nav_support_menu.html
+++ b/reviewboard/templates/base/_nav_support_menu.html
@@ -31,7 +31,7 @@
   <li><a href="{% url 'login' %}?next={{request.path}}">{% trans "Log in" %}</a></li>
 {#  XXX Using default sucks, but siteconfig defaults don't #}
 {#      work from templates.                               #}
-{%  if auth_backends.0.supports_registration and siteconfig_settings.auth_enable_registration|default_if_none:1 %}
+{%  if auth_backends.0.supports_registration and siteconfig_settings.auth_enable_registration|default_if_none:1 and not siteconfig_settings.site_read_only %}
   <li><a href="{% url 'register' %}">{% trans "Register" %}</a></li>
 {%  endif %}
 {% endif %}{# !is_authenticated #}
diff --git a/reviewboard/urls.py b/reviewboard/urls.py
index 6aee9dfe53a6b5a849e1e5d4f1c47e288db0ad5a..1a47d84e2f67537186ac2094c69b092268b64264 100644
--- a/reviewboard/urls.py
+++ b/reviewboard/urls.py
@@ -130,3 +130,12 @@ urlpatterns += patterns(
 )
 
 urlpatterns += localsite_urlpatterns
+
+# Read-only mode.
+urlpatterns += patterns(
+    '',
+
+    url(r'^503/$',
+        TemplateView.as_view(template_name='503.html'),
+        name='503'),
+)
