# Hardening SSH With Ansible

**URL:** <https://forum.ansible.com/t/hardening-ssh-with-ansible/31198>\
**Category:** Ansible Project\
**Created:** [September 4, 2019, 1:55pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198 "2019-09-04T13:55:03Z")\
**Posts on this page:** 18\
**Page:** 1

<div class="post-metadata">

**Author:** ![Jon\_Adcock](https://avatars.discourse-cdn.com/v4/letter/j/bcef8e/32.png) [@Jon\_Adcock](https://forum.ansible.com/u/Jon_Adcock)\
**Post date:** [September 4, 2019, 1:55pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/1 "2019-09-04T13:55:03Z")

</div>

I think its common practice to “harden” SSH by running the following in one of your playbooks:

`

- name: Disallow root SSH access

lineinfile:  
dest: /etc/ssh/sshd\_config  
regexp: “^PermitRootLogin no”  
line: “PermitRootLogin no”  
state: present  
notify:

- restart sshd

`

The way I understand this, if “PermitRootLogin no” does not appear in the sshd\_config file, it’ll append that to the bottom of the file.

So in the scenario where Ansible finds “PermitRootLogin yes” in the file, it will append “PermitRootLogin no” at the end of the config file.

The problem is that when SSH reads “PermitRootLogin yes” earlier in the file THAT configuration setting it what it uses. So the ‘no’ setting  
at the end of the file is ignored, and Ansible is not doing anything for me. ([http://man.openbsd.org/sshd\_config.5](http://man.openbsd.org/sshd_config.5))

---

<div class="post-metadata">

**Author:** ![racke](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/racke/32/4767_2.png) [@racke](https://forum.ansible.com/u/racke)\
**Post date:** [September 4, 2019, 2:10pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/2 "2019-09-04T14:10:01Z")

</div>

> I think its common practice to "harden" SSH by running the following in one of your playbooks:  
> \>  
> - name: Disallow root SSH access  
> lineinfile:  
> dest: /etc/ssh/sshd\_config  
> regexp: "^PermitRootLogin no"  
> line: "PermitRootLogin no"  
> state: present  
> notify:  
> - restart sshd
> 
> \>
> 
> The way I understand this, if "PermitRootLogin no" does not appear in the sshd\_config file, it'll append that to the  
> bottom of the file.
> 
> So in the scenario where Ansible finds "PermitRootLogin yes" in the file, it will append "PermitRootLogin no" at the end  
> of the config file.
> 
> The problem is that when SSH reads "PermitRootLogin yes" earlier in the file THAT configuration setting it what it  
> uses. So the 'no' setting  
> at the end of the file is ignored, and Ansible is not doing anything for me. ([http://man.openbsd.org/sshd\_config.5](http://man.openbsd.org/sshd_config.5))

Hello Jon,

you need to adjust the regexp so it covers both "yes" and "no" as configuration values. Or just drop "no" from the  
end of the regexp.

Regards  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Racke

---

<div class="post-metadata">

**Author:** ![Jonathan\_Lozada\_De\_L](https://avatars.discourse-cdn.com/v4/letter/j/eb8c5e/32.png) [@Jonathan\_Lozada\_De\_L](https://forum.ansible.com/u/Jonathan_Lozada_De_L)\
**Post date:** [September 4, 2019, 2:11pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/3 "2019-09-04T14:11:23Z")

</div>

I used jinja2 template to solve some of this issues.

---

<div class="post-metadata">

**Author:** ![Jon\_Adcock](https://avatars.discourse-cdn.com/v4/letter/j/bcef8e/32.png) [@Jon\_Adcock](https://forum.ansible.com/u/Jon_Adcock)\
**Post date:** [September 4, 2019, 2:50pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/4 "2019-09-04T14:50:42Z")

</div>

Racke,

I’m not sure how that helps? Ansible will still add the ‘no’ setting at the bottom of the sshd\_config file and the ‘yes’ setting earlier in the sshd\_config file is still active.

Thanks,  
Jon

---

<div class="post-metadata">

**Author:** ![Kai\_Stian\_Olstad](https://avatars.discourse-cdn.com/v4/letter/k/e19adc/32.png) [@Kai\_Stian\_Olstad](https://forum.ansible.com/u/Kai_Stian_Olstad)\
**Post date:** [September 4, 2019, 3:51pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/5 "2019-09-04T15:51:22Z")

</div>

It will replace the line, you test it and you'll see.

---

<div class="post-metadata">

**Author:** ![Jon\_Adcock](https://avatars.discourse-cdn.com/v4/letter/j/bcef8e/32.png) [@Jon\_Adcock](https://forum.ansible.com/u/Jon_Adcock)\
**Post date:** [September 4, 2019, 7:55pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/6 "2019-09-04T19:55:13Z")

</div>

Kai, I tried removed the no from my regexp and retested. Ansible did replace that line! It did not add a new entry at the end of the config file.  
Now I have to go figure out WHO changed the config file from no to yes.  
Thank you.

---

<div class="post-metadata">

**Author:** ![cyril.stoll](https://avatars.discourse-cdn.com/v4/letter/c/b4bc9f/32.png) [@cyril.stoll](https://forum.ansible.com/u/cyril.stoll)\
**Post date:** [September 5, 2019, 8:07am UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/7 "2019-09-05T08:07:36Z")

</div>

Hi there

---

<div class="post-metadata">

**Author:** ![cyril.stoll](https://avatars.discourse-cdn.com/v4/letter/c/b4bc9f/32.png) [@cyril.stoll](https://forum.ansible.com/u/cyril.stoll)\
**Post date:** [September 5, 2019, 8:10am UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/8 "2019-09-05T08:10:17Z")

</div>

Small correction:

> regexp: “^(#|#\s|\s|)PermitRootLogin(\s\*)(no|yes)”

The brackets around \s\* are not needed. So this is correct:

regexp: “^(#|#\s|\s|)PermitRootLogin\s\*(no|yes)”

---

<div class="post-metadata">

**Author:** ![system](https://us1.discourse-cdn.com/ansible/original/1X/1882d9a01da5d8996a862359b0913cc568a39b84.svg) [@system](https://forum.ansible.com/u/system)\
**Post date:** [September 5, 2019, 8:46am UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/9 "2019-09-05T08:46:52Z")

</div>

Actually \s\* is not _one or many_, but _zero or many_. There should be at least one space so `\s+`

---

<div class="post-metadata">

**Author:** ![Jon\_Adcock](https://avatars.discourse-cdn.com/v4/letter/j/bcef8e/32.png) [@Jon\_Adcock](https://forum.ansible.com/u/Jon_Adcock)\
**Post date:** [September 5, 2019, 11:46am UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/10 "2019-09-05T11:46:14Z")

</div>

Cyril,when I tried your regex and Ansible barked about the \s Both before and after PermitRootLogin I tried double bask-slash s (#|#\s|\s|) and that works.

---

<div class="post-metadata">

**Author:** ![S\_C\_Rigler](https://avatars.discourse-cdn.com/v4/letter/s/eb9ed0/32.png) [@S\_C\_Rigler](https://forum.ansible.com/u/S_C_Rigler)\
**Post date:** [September 5, 2019, 1:39pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/11 "2019-09-05T13:39:27Z")

</div>

All of these regex’s seem a little too specific. Why not just:

regexp: ‘^PermitRootLogin\b’  
line: ‘PermitRootLogin no’

If the entry isn’t there it will add it (who cares if a commented entry already exists). If it already exists and has a different value it will replace the line with the new value.

---

<div class="post-metadata">

**Author:** ![cyril.stoll](https://avatars.discourse-cdn.com/v4/letter/c/b4bc9f/32.png) [@cyril.stoll](https://forum.ansible.com/u/cyril.stoll)\
**Post date:** [September 5, 2019, 1:47pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/12 "2019-09-05T13:47:14Z")

</div>

@Jon glad you found a solution that works for you. Strangely I don’t have issues with just one backslash. Might be due to the old python version that is used on macOS.

@Stefan thanks for the correction!

@SCRigler basically you are right. But if someone sees the line that is commented out she/he might think the setting fell back to the default setting and close the config file again without noticing that way down in the config file there is actually a working setting that does overwrite the default. Might make troubleshooting a bit more frustrating that way.

![Inactive hide details for "S C Rigler" ---05.09.2019 15:39:57---All of these regex's seem a little too specific. Why not just:](https://us1.discourse-cdn.com/ansible/original/2X/8/8d131f16ac63f0a9c083c1291a79b729af029b34.gif)“S C Rigler” —05.09.2019 15:39:57—All of these regex’s seem a little too specific. Why not just: regexp: ‘^PermitRootLogin\b’

---

<div class="post-metadata">

**Author:** ![vbotka](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/vbotka/32/359_2.png) [@vbotka](https://forum.ansible.com/u/vbotka)\
**Post date:** [September 5, 2019, 1:55pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/13 "2019-09-05T13:55:09Z")

</div>

Hi !

> \> regexp: "^(#|#\s|\s|)PermitRootLogin\s\*(no|yes)"  
> Actually \s\* is not \*one or many\*, but \*zero or many\*. There should be at  
> least one space so `\s+`

1) Searching for both commented and non-commented lines is risky. There  
&nbsp;&nbsp;&nbsp;might be both present in the file and the result will be unpredictable.  
&nbsp;&nbsp;&nbsp;It's better to ignore commented lines.

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;regexp: "^(\s\*)PermitRootLogin\s+(no|yes)"

2) Searching for (no|yes) does not make any sense. We're going to replace it  
&nbsp;&nbsp;&nbsp;anyway. This may make things even worse by preventing a potential syntax  
&nbsp;&nbsp;&nbsp;error to be corrected. Replace it with general pattern.

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;regexp: "^(\s\*)PermitRootLogin\s+(.\*)$"

3) Always validate the configuration

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;validate: "{{ sshd\_path }} -t -f %s"

my two cents, Cheers,

&nbsp;&nbsp;-vlado

---

<div class="post-metadata">

**Author:** ![Jon\_Adcock](https://avatars.discourse-cdn.com/v4/letter/j/bcef8e/32.png) [@Jon\_Adcock](https://forum.ansible.com/u/Jon_Adcock)\
**Post date:** [September 5, 2019, 3:28pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/14 "2019-09-05T15:28:36Z")

</div>

Vlado, It’s my understanding that the validate step should be done once in the playbook. Not for each sshd action in the playbook. Is that what you are saying?

---

<div class="post-metadata">

**Author:** ![Kai\_Stian\_Olstad](https://avatars.discourse-cdn.com/v4/letter/k/e19adc/32.png) [@Kai\_Stian\_Olstad](https://forum.ansible.com/u/Kai_Stian_Olstad)\
**Post date:** [September 5, 2019, 4:27pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/15 "2019-09-05T16:27:44Z")

</div>

That is because you are using double quotes and not single quotes around the regexp.  
You could also remove the quotes since you don't actually need them.

---

<div class="post-metadata">

**Author:** ![Kai\_Stian\_Olstad](https://avatars.discourse-cdn.com/v4/letter/k/e19adc/32.png) [@Kai\_Stian\_Olstad](https://forum.ansible.com/u/Kai_Stian_Olstad)\
**Post date:** [September 5, 2019, 4:35pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/16 "2019-09-05T16:35:44Z")

</div>

If you are going to do hardening and have some kind reliability that it work you should not be using the lineinfile module.  
Instead go for the copy or template module that Jonathan mention previously in this thread.

The reason for that is that lineinfile \_\*only\*\_ changes the last line it find in the file and openssh only uses the first line it find.

So you Ansible code can easily be tricked by these lines.

PermitRootLogin yes  
PermitRootLogin no

or

PermitRootLogin yes  
#PermitRootLogin yes

And many other combination.

---

<div class="post-metadata">

**Author:** ![vbotka](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/vbotka/32/359_2.png) [@vbotka](https://forum.ansible.com/u/vbotka)\
**Post date:** [September 5, 2019, 5:55pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/17 "2019-09-05T17:55:59Z")

</div>

Jon, how this might be accomplished? "validate" by design says:  
[https://docs.ansible.com/ansible/latest/modules/lineinfile\_module.html](https://docs.ansible.com/ansible/latest/modules/lineinfile_module.html)

&nbsp;&nbsp;"The validation command to run before copying into place..."

It works for me fine that way. The burden of the repeated validation is  
present in the first run only (when idempotent). It's worth to be sure sshd  
will keep running.

Cheers,

&nbsp;&nbsp;-vlado

---

<div class="post-metadata">

**Author:** ![Mike\_Eggleston](https://avatars.discourse-cdn.com/v4/letter/m/58956e/32.png) [@Mike\_Eggleston](https://forum.ansible.com/u/Mike_Eggleston)\
**Post date:** [September 5, 2019, 6:27pm UTC](https://forum.ansible.com/t/hardening-ssh-with-ansible/31198/18 "2019-09-05T18:27:48Z")

</div>

And the presented regents is case specific. Try “(?i)...”.

Mike
