Added SESAME_TOTP_SECRET (Issue #100)

This commit is contained in:
Daniel Perna 2018-07-11 01:05:33 +02:00
parent ae9b645500
commit b04eb10682
4 changed files with 25 additions and 5 deletions

View file

@ -75,6 +75,8 @@ Verify, that the user that is running the configurator is allowed to push withou
If set to `true`, directories will be displayed at the top. If set to `true`, directories will be displayed at the top.
#### SESAME (string) #### SESAME (string)
If set to _somesecretkeynobodycanguess_, you can browse to `https://your.configurator:3218/somesecretkeynobodycanguess` from any IP, and it will be removed from the `BANNED_IPS` list (in case it has been banned before) and added to the `ALLOWED_NETWORKS` list. Once the request has been processed you will automatically be redirected to the configurator. Think of this as dynamically allowing access from untrusted IPs by providing a secret key (_open sesame!_). Keep in mind, that once the IP has been added, you will either have to restart the configurator or manually remove the IP through the _Network status_ to revoke access. If set to _somesecretkeynobodycanguess_, you can browse to `https://your.configurator:3218/somesecretkeynobodycanguess` from any IP, and it will be removed from the `BANNED_IPS` list (in case it has been banned before) and added to the `ALLOWED_NETWORKS` list. Once the request has been processed you will automatically be redirected to the configurator. Think of this as dynamically allowing access from untrusted IPs by providing a secret key (_open sesame!_). Keep in mind, that once the IP has been added, you will either have to restart the configurator or manually remove the IP through the _Network status_ to revoke access.
#### SESAME_TOTP_SECRET (string)
Instead of or additionally to the `SESAME` token you may also specify a [Base32](https://en.wikipedia.org/wiki/Base32) encoded string that serves as the token for time based OTP (one time password) IP whitelisting. It works like the regular `SESAME`, but the request path that whitelists your IP changes every 30 seconds. You can add the `SESAME_TOTP_SECRET` to most of the available OTP-Apps (Google Authenticator and alike) and just append the 6-digit number to the URI where your configurator is reachable. For this to work the [pyotp](https://github.com/pyotp/pyotp) module has to be installed.
#### VERIFY_HOSTNAME (string) #### VERIFY_HOSTNAME (string)
HTTP requests include the hostname to which the request has been made. To improve security you can set this parameter to `yourdomain.example.com`. This will check if the hostname within the request matches the one you are expecting. If it does not match, a `403 Forbidden` response will be sent. As a result attackers that scan your IP address won't be able to connect unless they know the correct hostname. Be careful with this option though, because it prohibits you from accessing the configurator directly via IP. HTTP requests include the hostname to which the request has been made. To improve security you can set this parameter to `yourdomain.example.com`. This will check if the hostname within the request matches the one you are expecting. If it does not match, a `403 Forbidden` response will be sent. As a result attackers that scan your IP address won't be able to connect unless they know the correct hostname. Be careful with this option though, because it prohibits you from accessing the configurator directly via IP.
#### ENV_PREFIX (string) #### ENV_PREFIX (string)

View file

@ -5,6 +5,7 @@ Version 0.3.0 (2018-)
- Notifying if used passwords are insecure (Issue #100) and if SESAME has been used @danielperna84 - Notifying if used passwords are insecure (Issue #100) and if SESAME has been used @danielperna84
- CREDENTIALS setting replaced by USERNAME and PASSWORD @danielperna84 - CREDENTIALS setting replaced by USERNAME and PASSWORD @danielperna84
- PASSWORD can optionally be provided as SHA256 hash (Issue #100) @danielperna84 - PASSWORD can optionally be provided as SHA256 hash (Issue #100) @danielperna84
- Added SESAME_TOTP_SECRET for TOTP based IP whitelisting (Issue #100) @danielperna84
Version 0.2.9 (2018-06-22) Version 0.2.9 (2018-06-22)
- Material Icons and HASS-help now open in new tab instead of modal (Issues #85 and #34) @danielperna84 - Material Icons and HASS-help now open in new tab instead of modal (Issues #85 and #34) @danielperna84

View file

@ -69,6 +69,9 @@ DIRSFIRST = False
# Sesame token. Browse to the configurator URL + /secrettoken to unban your # Sesame token. Browse to the configurator URL + /secrettoken to unban your
# client IP and add it to the list of allowed IPs. # client IP and add it to the list of allowed IPs.
SESAME = None SESAME = None
# Instead of a static SESAME token you may also use a TOTP based token that
# changes every 30 seconds. The value needs to be a base 32 encoded string.
SESAME_TOTP_SECRET = None
# Verify the hostname used in the request. Block access if it doesn't match # Verify the hostname used in the request. Block access if it doesn't match
# this value # this value
VERIFY_HOSTNAME = None VERIFY_HOSTNAME = None
@ -91,14 +94,16 @@ RELEASEURL = "https://api.github.com/repos/danielperna84/hass-configurator/relea
VERSION = "0.3.0" VERSION = "0.3.0"
BASEDIR = "." BASEDIR = "."
DEV = False DEV = False
TOTP = None
HTTPD = None HTTPD = None
FAIL2BAN_IPS = {} FAIL2BAN_IPS = {}
REPO = False REPO = False
if GIT: if GIT:
try: try:
from git import Repo as REPO from git import Repo as REPO
except Exception: except ImportError:
LOG.warning("Unable to import Git module") LOG.warning("Unable to import Git module")
INDEX = Template(r"""<!DOCTYPE html> INDEX = Template(r"""<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -3395,7 +3400,7 @@ def load_settings(settingsfile):
global LISTENIP, LISTENPORT, BASEPATH, SSL_CERTIFICATE, SSL_KEY, HASS_API, \ global LISTENIP, LISTENPORT, BASEPATH, SSL_CERTIFICATE, SSL_KEY, HASS_API, \
HASS_API_PASSWORD, CREDENTIALS, ALLOWED_NETWORKS, BANNED_IPS, BANLIMIT, \ HASS_API_PASSWORD, CREDENTIALS, ALLOWED_NETWORKS, BANNED_IPS, BANLIMIT, \
DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME, ENFORCE_BASEPATH, \ DEV, IGNORE_PATTERN, DIRSFIRST, SESAME, VERIFY_HOSTNAME, ENFORCE_BASEPATH, \
ENV_PREFIX, NOTIFY_SERVICE, USERNAME, PASSWORD ENV_PREFIX, NOTIFY_SERVICE, USERNAME, PASSWORD, SESAME_TOTP_SECRET, TOTP
settings = {} settings = {}
if settingsfile: if settingsfile:
try: try:
@ -3437,6 +3442,7 @@ def load_settings(settingsfile):
IGNORE_PATTERN = settings.get("IGNORE_PATTERN", IGNORE_PATTERN) IGNORE_PATTERN = settings.get("IGNORE_PATTERN", IGNORE_PATTERN)
DIRSFIRST = settings.get("DIRSFIRST", DIRSFIRST) DIRSFIRST = settings.get("DIRSFIRST", DIRSFIRST)
SESAME = settings.get("SESAME", SESAME) SESAME = settings.get("SESAME", SESAME)
SESAME_TOTP_SECRET = settings.get("SESAME_TOTP_SECRET", SESAME_TOTP_SECRET)
VERIFY_HOSTNAME = settings.get("VERIFY_HOSTNAME", VERIFY_HOSTNAME) VERIFY_HOSTNAME = settings.get("VERIFY_HOSTNAME", VERIFY_HOSTNAME)
NOTIFY_SERVICE = settings.get("NOTIFY_SERVICE", NOTIFY_SERVICE_DEFAULT) NOTIFY_SERVICE = settings.get("NOTIFY_SERVICE", NOTIFY_SERVICE_DEFAULT)
USERNAME = settings.get("USERNAME", USERNAME) USERNAME = settings.get("USERNAME", USERNAME)
@ -3446,6 +3452,14 @@ def load_settings(settingsfile):
PASSWORD = ":".join(CREDENTIALS.split(":")[1:]) PASSWORD = ":".join(CREDENTIALS.split(":")[1:])
if PASSWORD and PASSWORD.startswith("{sha256}"): if PASSWORD and PASSWORD.startswith("{sha256}"):
PASSWORD = PASSWORD.lower() PASSWORD = PASSWORD.lower()
if SESAME_TOTP_SECRET:
try:
import pyotp
TOTP = pyotp.TOTP(SESAME_TOTP_SECRET)
except ImportError:
LOG.warning("Unable to import pyotp module")
except Exception as err:
LOG.warning("Unable to create TOTP object: %s" % err)
def is_safe_path(basedir, path, follow_symlinks=True): def is_safe_path(basedir, path, follow_symlinks=True):
if basedir is None: if basedir is None:
@ -3588,13 +3602,14 @@ class RequestHandler(BaseHTTPRequestHandler):
self.do_BLOCK(403, "Forbidden") self.do_BLOCK(403, "Forbidden")
return return
req = urlparse(self.path) req = urlparse(self.path)
if SESAME: if SESAME or TOTP:
if req.path.endswith("/%s" % SESAME): chunk = req.path.split("/")[-1]
if chunk == SESAME or TOTP.verify(chunk):
if self.client_address[0] not in ALLOWED_NETWORKS: if self.client_address[0] not in ALLOWED_NETWORKS:
ALLOWED_NETWORKS.append(self.client_address[0]) ALLOWED_NETWORKS.append(self.client_address[0])
if self.client_address[0] in BANNED_IPS: if self.client_address[0] in BANNED_IPS:
BANNED_IPS.remove(self.client_address[0]) BANNED_IPS.remove(self.client_address[0])
url = req.path[:req.path.rfind(SESAME)] url = req.path[:req.path.rfind(chunk)]
self.send_response(302) self.send_response(302)
self.send_header('Location', url) self.send_header('Location', url)
self.end_headers() self.end_headers()
@ -4590,6 +4605,7 @@ def notify(title="HASS Configurator",
"%sservices/%s" % (HASS_API, NOTIFY_SERVICE.replace('.', '/')), "%sservices/%s" % (HASS_API, NOTIFY_SERVICE.replace('.', '/')),
data=bytes(json.dumps(data).encode('utf-8')), data=bytes(json.dumps(data).encode('utf-8')),
headers=headers, method='POST') headers=headers, method='POST')
LOG.info("%s" % data)
try: try:
with urllib.request.urlopen(req) as response: with urllib.request.urlopen(req) as response:
message = response.read().decode('utf-8') message = response.read().decode('utf-8')

View file

@ -15,6 +15,7 @@
"IGNORE_PATTERN": [], "IGNORE_PATTERN": [],
"DIRSFIRST": false, "DIRSFIRST": false,
"SESAME": null, "SESAME": null,
"SESAME_TOTP_SECRET": null,
"VERIFY_HOSTNAME": null, "VERIFY_HOSTNAME": null,
"ENV_PREFIX": "HC_", "ENV_PREFIX": "HC_",
"NOTIFY_SERVICE": "persistent_notification.create" "NOTIFY_SERVICE": "persistent_notification.create"