New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Advanced Ip filtering #4424
Advanced Ip filtering #4424
Conversation
@vkorn, thanks for your PR! By analyzing the history of the files in this pull request, we identified @balloob, @mweinelt and @JshWright to be potential reviewers. |
trusted_networks=[], | ||
ip_bans=[], | ||
login_threshold=0, | ||
is_ban_enabled=False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize that there are already mutable types in the kwargs in this function, but that in general is bad python and causes some gnarly bugs. Here is an example of why doing this is a very bad idea:
[jeff@omniscience ~]$ python3
Python 3.5.2 (default, Oct 14 2016, 12:54:53)
[GCC 6.2.1 20160916 (Red Hat 6.2.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def append_one(data=[]):
... data.append(1)
... return data
...
>>> append_one()
[1]
>>> append_one()
[1, 1]
>>> append_one()
[1, 1, 1]
>>> append_one(data=[2])
[2, 1]
>>> append_one()
[1, 1, 1, 1]
>>> append_one()
[1, 1, 1, 1, 1]
>>>
Since those variables such as trusted_networks
or ip_bans
are initialized (at module load time) with mutable types, the values stay as though they're globals along the life of the process. This means it stays the life of the entire hass daemon unless those kwargs are passed at function call time.
In theory setup() should only run once, but even still if possible can you not do this? I think setting them to None
and then checking in the body of the function, while more verbose, will prevent an entire class of bugs related to object mutability in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SEJeff, fixed in recent commit, please review.
👍 from me. @balloob ? |
Nice feature. |
@Danielhiversen, |
@@ -386,6 +411,38 @@ def is_trusted_ip(self, remote_addr): | |||
return any(ip_address(remote_addr) in trusted_network | |||
for trusted_network in self.hass.http.trusted_networks) | |||
|
|||
def wrong_login_attempt(self, remote_addr): | |||
"""Registering wrong login attempt.""" | |||
if not self.is_trusted_ip(remote_addr) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider rewriting this with guard clauses. Every time you know you will never do anything besides the if
statement, including not using an else
statement, you can just reverse the if
and return.
if self.is_trusted_ip(remote_addr):
return
etc…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would a trusted ip ever be able to get into the wrong_login_attempt
method ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're correct, trusted IP get authenticated = True
-- removing this check
|
||
try: | ||
list_ = load_yaml_config_file(path) | ||
for ip_ban in list_: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can validate a list like this:
vol.Schema([vol.Schema({…})])
ban = ip_schema(list_[ip_ban]) | ||
ban['ip_ban'] = ip_address(ip_ban) | ||
ip_list.append(IpBan(**ban)) | ||
except vol.Invalid: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should report if something is invalid or no one will know
continue | ||
|
||
except(HomeAssistantError, FileNotFoundError): | ||
return [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should report when a HomeAssistantError occurs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, adding check prior to this. For FileNotFount it doesn't make sense to report -- it just means that no bans have being applied
"""Validate timestamp.""" | ||
if isinstance(value, date) or isinstance(value, datetime): | ||
return value | ||
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only support isoformat
and the inverse dt_util.parse_datetime(…)
@@ -297,6 +297,13 @@ def time(value): | |||
return time_val | |||
|
|||
|
|||
def exact_time(value): | |||
"""Validate timestamp.""" | |||
if isinstance(value, date) or isinstance(value, datetime): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When would this happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example if there's no quotes on value:
banned_at: 2016-11-23T18:50:33
Could be reason of manual adding. Probably I should remove check for date
instance and leave only datetime, otherwise validation is misleading
@@ -297,6 +297,13 @@ def time(value): | |||
return time_val | |||
|
|||
|
|||
def exact_time(value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datetime
is the correct description
remote_addr) | ||
persistent_notification.async_create( | ||
self.hass, | ||
'To many login attempts from {}'.format(remote_addr), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Too"
I just setup external access to my home assistant and exempted my home network.. I was thinking of setting up fail2ban but a native solution sounds great. Two question:
|
|
Sounds great... Home assistant uses a web form for authentication but it probably wont be too long before a bot starts filling it in and failing.. working with fail2ban and other IP ban scripts the cleanup eventually becomes important. Another reason as well is to not permanently lock your self out, some systems have a soft ban and a hard ban... IE such as X consecutive attempts results in a non-permanent 5-10min ban, however if there are additional attempts it moves into a permanent / long term ban. You may also want an IP ban whitelist for users that have the API password enabled at all times, as they may locally lock them self out otherwise. |
Description:
Implementation of automatic IP ban in case of multiple failed login attempts. Partially related to this request.
File
ip_bans.yaml
automatically created in config folder.Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#1450
Example entry for
configuration.yaml
(if applicable):Two new params in http component:
Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass