Skip to content
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

Merged
merged 7 commits into from Nov 25, 2016
Merged

Advanced Ip filtering #4424

merged 7 commits into from Nov 25, 2016

Conversation

vkorn
Copy link
Contributor

@vkorn vkorn commented Nov 16, 2016

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:

http:
  ip_ban_enabled: True
  login_attempts_threshold: 2

Checklist:

If user exposed functionality or configuration variables are added/changed:

If the code does not interact with devices:

  • Local tests with tox run successfully. Your PR cannot be merged unless tests pass
  • Tests have been added to verify that the new code works.

@mention-bot
Copy link

@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
Copy link
Contributor

@SEJeff SEJeff Nov 19, 2016

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.

Copy link
Contributor Author

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.

@SEJeff
Copy link
Contributor

SEJeff commented Nov 21, 2016

👍 from me. @balloob ?

@Danielhiversen
Copy link
Member

Danielhiversen commented Nov 21, 2016

Nice feature.
Is the ip_ban_enabled parameter necessary?
login_attempts_threshold < 1 -> disable ip ban functionality?

@vkorn
Copy link
Contributor Author

vkorn commented Nov 22, 2016

@Danielhiversen, ip_ban enabled responsible for rejecting all request from ip_bans.yaml file.
If login_attempts_threshold >= 1 it will automatically ban any new failed IPs.

@@ -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) \
Copy link
Member

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

Copy link
Member

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 ?

Copy link
Contributor Author

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_:
Copy link
Member

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:
Copy link
Member

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 []
Copy link
Member

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

Copy link
Contributor Author

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")
Copy link
Member

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this happen?

Copy link
Contributor Author

@vkorn vkorn Nov 23, 2016

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):
Copy link
Member

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Too"

@bagobones
Copy link

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:

  1. Is this change compatible with the X-forwarded for option?
  2. I think there should be a parameter / default expiry of banned IPs.. From experience a lot of late nite scans happen from many random cloud hosted IPs. I am afraid that without an automatic clean up the IP ban file could get very very large.

@vkorn
Copy link
Contributor Author

vkorn commented Nov 24, 2016

@bagobones,

  1. Yes
  2. Didn't think about it, basically haven't notice such problems yet. Can implement something after this PR will be merged as another PR.

@bagobones
Copy link

@vkorn

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.

@balloob balloob merged commit 2a7bc0e into home-assistant:dev Nov 25, 2016
@home-assistant home-assistant locked and limited conversation to collaborators Mar 17, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants