Set a cookie for /admin/munin pages to grant access to Munin reports
The /admin/munin routes used the same Authorization: header logic as the other API routes, but they are browsed directly in the browser because they are handled as static pages or as a proxy to a CGI script. This required users to enter their email username/password for HTTP basic authentication in the standard browser auth prompt, which wasn't ideal (and may leak the password in browser storage). It also stopped working when MFA was enabled for user accounts. A token is now set in a cookie when visiting /admin/munin which is then checked in the routes that proxy the Munin pages. The cookie's lifetime is kept limited to limit the opportunity for any unknown CSRF attacks via the Munin CGI script.
This commit is contained in:
parent
66b15d42a5
commit
79966e36e3
5 changed files with 67 additions and 12 deletions
|
@ -19,6 +19,7 @@ Control panel:
|
||||||
* After logging in, the default page is now a fast-loading welcome page rather than the slow-loading system status checks page.
|
* After logging in, the default page is now a fast-loading welcome page rather than the slow-loading system status checks page.
|
||||||
* The backup retention period option now displays for B2 backup targets.
|
* The backup retention period option now displays for B2 backup targets.
|
||||||
* The DNSSEC DS record recommendations are cleaned up and now recommend changing records that use SHA1.
|
* The DNSSEC DS record recommendations are cleaned up and now recommend changing records that use SHA1.
|
||||||
|
* The Munin monitoring pages no longer require a separate HTTP basic authentication login and can be used if two-factor authentication is turned on.
|
||||||
* Control panel logins are now tied to a session backend that allows true logouts (rather than an encrypted cookie).
|
* Control panel logins are now tied to a session backend that allows true logouts (rather than an encrypted cookie).
|
||||||
* Failed logins no longer directly reveal whether the email address corresponds to a user account.
|
* Failed logins no longer directly reveal whether the email address corresponds to a user account.
|
||||||
* Browser dark mode now inverts the color scheme.
|
* Browser dark mode now inverts the color scheme.
|
||||||
|
|
|
@ -72,14 +72,9 @@ class AuthService:
|
||||||
return (None, ["admin"])
|
return (None, ["admin"])
|
||||||
|
|
||||||
# If the password corresponds with a session token for the user, grant access for that user.
|
# If the password corresponds with a session token for the user, grant access for that user.
|
||||||
if password in self.sessions and self.sessions[password]["email"] == username and not login_only:
|
if self.get_session(username, password, "login", env) and not login_only:
|
||||||
sessionid = password
|
sessionid = password
|
||||||
session = self.sessions[sessionid]
|
session = self.sessions[sessionid]
|
||||||
if session["password_token"] != self.create_user_password_state_token(username, env):
|
|
||||||
# This session is invalid because the user's password/MFA state changed
|
|
||||||
# after the session was created.
|
|
||||||
del self.sessions[sessionid]
|
|
||||||
raise ValueError("Session expired.")
|
|
||||||
if logout:
|
if logout:
|
||||||
# Clear the session.
|
# Clear the session.
|
||||||
del self.sessions[sessionid]
|
del self.sessions[sessionid]
|
||||||
|
@ -158,5 +153,14 @@ class AuthService:
|
||||||
self.sessions[token] = {
|
self.sessions[token] = {
|
||||||
"email": username,
|
"email": username,
|
||||||
"password_token": self.create_user_password_state_token(username, env),
|
"password_token": self.create_user_password_state_token(username, env),
|
||||||
|
"type": type,
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
def get_session(self, user_email, session_key, session_type, env):
|
||||||
|
if session_key not in self.sessions: return None
|
||||||
|
session = self.sessions[session_key]
|
||||||
|
if session_type == "login" and session["email"] != user_email: return None
|
||||||
|
if session["type"] != session_type: return None
|
||||||
|
if session["password_token"] != self.create_user_password_state_token(session["email"], env): return None
|
||||||
|
return session
|
||||||
|
|
|
@ -654,16 +654,42 @@ def privacy_status_set():
|
||||||
# MUNIN
|
# MUNIN
|
||||||
|
|
||||||
@app.route('/munin/')
|
@app.route('/munin/')
|
||||||
@app.route('/munin/<path:filename>')
|
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def munin(filename=""):
|
def munin_start():
|
||||||
# Checks administrative access (@authorized_personnel_only) and then just proxies
|
# Munin pages, static images, and dynamically generated images are served
|
||||||
# the request to static files.
|
# outside of the AJAX API. We'll start with a 'start' API that sets a cookie
|
||||||
|
# that subsequent requests will read for authorization. (We don't use cookies
|
||||||
|
# for the API to avoid CSRF vulnerabilities.)
|
||||||
|
response = make_response("OK")
|
||||||
|
response.set_cookie("session", auth_service.create_session_key(request.user_email, env, type='cookie'),
|
||||||
|
max_age=60*30, secure=True, httponly=True, samesite="Strict") # 30 minute duration
|
||||||
|
return response
|
||||||
|
|
||||||
|
def check_request_cookie_for_admin_access():
|
||||||
|
session = auth_service.get_session(None, request.cookies.get("session", ""), "cookie", env)
|
||||||
|
if not session: return False
|
||||||
|
privs = get_mail_user_privileges(session["email"], env)
|
||||||
|
if not isinstance(privs, list): return False
|
||||||
|
if "admin" not in privs: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def authorized_personnel_only_via_cookie(f):
|
||||||
|
@wraps(f)
|
||||||
|
def g(*args, **kwargs):
|
||||||
|
if not check_request_cookie_for_admin_access():
|
||||||
|
return Response("Unauthorized", status=403, mimetype='text/plain', headers={})
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return g
|
||||||
|
|
||||||
|
@app.route('/munin/<path:filename>')
|
||||||
|
@authorized_personnel_only_via_cookie
|
||||||
|
def munin_static_file(filename=""):
|
||||||
|
# Proxy the request to static files.
|
||||||
if filename == "": filename = "index.html"
|
if filename == "": filename = "index.html"
|
||||||
return send_from_directory("/var/cache/munin/www", filename)
|
return send_from_directory("/var/cache/munin/www", filename)
|
||||||
|
|
||||||
@app.route('/munin/cgi-graph/<path:filename>')
|
@app.route('/munin/cgi-graph/<path:filename>')
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only_via_cookie
|
||||||
def munin_cgi(filename):
|
def munin_cgi(filename):
|
||||||
""" Relay munin cgi dynazoom requests
|
""" Relay munin cgi dynazoom requests
|
||||||
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
|
/usr/lib/munin/cgi/munin-cgi-graph is a perl cgi script in the munin package
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
<li class="dropdown-header">Advanced Pages</li>
|
<li class="dropdown-header">Advanced Pages</li>
|
||||||
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
||||||
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
|
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
|
||||||
<li><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
|
<li><a href="#munin" onclick="return show_panel(this);">Munin Monitoring</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="#mail-guide" onclick="return show_panel(this);" class="if-logged-in-not-admin">Mail</a></li>
|
<li><a href="#mail-guide" onclick="return show_panel(this);" class="if-logged-in-not-admin">Mail</a></li>
|
||||||
|
@ -202,6 +202,10 @@
|
||||||
{% include "ssl.html" %}
|
{% include "ssl.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="panel_munin" class="admin_panel">
|
||||||
|
{% include "munin.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|
20
management/templates/munin.html
Normal file
20
management/templates/munin.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<h2>Munin Monitoring</h2>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<p>Opening munin in a new tab... You may need to allow pop-ups for this site.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function show_munin() {
|
||||||
|
// Set the cookie.
|
||||||
|
api(
|
||||||
|
"/munin",
|
||||||
|
"GET",
|
||||||
|
{ },
|
||||||
|
function(r) {
|
||||||
|
// Redirect.
|
||||||
|
window.open("/admin/munin/index.html", "_blank");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in a new issue