5

During the process of setting up my Raspberries, I want to prevent root logins via ssh. As always, it's a "scriptlet" (called by a runner).

My research told me that, even in times of systemd, /etc/ssh/sshd_config is the file to modify. So far, so good. In my humble understanding, what needs to be done is this: read the config file line by line, match for "PermitRootLogin yes" (whitespace match); if no match, write the line to another file, if yes, replace it with "PermitRootLogin no", and write to the other file, and finally replace the original configuration file with the new file, and restart sshd via the systemd stuff.

In Perl, I'd read the whole file, replace() the line, and write stuff back to another file. But as the young Buddhist said: "There is no Perl!"

Bash(2) only, please.

tripleee
  • 158,107
  • 27
  • 234
  • 292
CarstenP
  • 123
  • 1
  • 8

7 Answers7

13

Script:

$ sed -i 's/PermitRootLogin yes/PermitRootLogin no/g' /etc/ssh/sshd_config

Then:

$ service ssh reload
Nick Tsai
  • 3,437
  • 30
  • 34
5

You can do something like that

#!/bin/bash
if [[ "${UID}" -ne 0 ]]; then
    echo " You need to run this script as root"
    exit 1
fi

# To directly modify sshd_config.

sed -i 's/#\?\(Port\s*\).*$/\1 2231/' /etc/ssh/sshd_config
sed -i 's/#\?\(PermitRootLogin\s*\).*$/\1 no/' /etc/ssh/sshd_config
sed -i 's/#\?\(PubkeyAuthentication\s*\).*$/\1 yes/' /etc/ssh/sshd_config
sed -i 's/#\?\(PermitEmptyPasswords\s*\).*$/\1 no/' /etc/ssh/sshd_config
sed -i 's/#\?\(PasswordAuthentication\s*\).*$/\1 no/' /etc/ssh/sshd_config

# Check the exit status of the last command

if [[ "${?}" -ne 0 ]]; then
   echo "The sshd_config file was not modified successfully"
   exit 1
fi
/etc/init.d/ssh restart

exit 0
Caesar
  • 3,590
  • 2
  • 27
  • 40
frontdl
  • 67
  • 1
  • 2
  • 1
    Repeatedly rewriting the file is inelegant and inefficient. See https://stackoverflow.com/questions/7657647/combining-two-sed-commands – tripleee Aug 19 '20 at 07:32
  • 1
    Also, https://stackoverflow.com/questions/36313216/why-is-testing-to-see-if-a-command-succeeded-or-not-an-anti-pattern – tripleee Aug 19 '20 at 07:38
  • PerminRootLogin is a typo, PermitRootLogin is better to use. But thanks the regex, it is helpful – scb Nov 17 '20 at 15:19
4

Configuring SSH programmatically

I did some investigation on this and I think JNevevill's answer is the most solid. I have some example script that illustrated the two ways, SED and AWK.

Preparation

Showing the test data only once for better readability in the examples. It has been initialized for each approach the same way. It will define some rules in a test file and a dictionary of new rules that should replace the rules in the file or be written to the file.

echo 'LoginGraceTime 120' > ./data.txt
echo '#PermitRootLogin yes' >> ./data.txt
echo 'PermitRootLogin no' >> ./data.txt
echo 'PasswordAuthentication yes' >> ./data.txt

declare -A rules=( 
    ["LoginGraceTime"]="1m"
    ["PermitRootLogin"]="no"
    ["PasswordAuthentication"]="no"
    ["AllowUsers"]="blue"
)

SED

SED will replace the rules it finds. If a line is commented, it will remove the # and replace the value. This approach is less solid as it can lead to duplicate rules. I.E. A rule exists commented and uncommented. Also, if the rule wasn't there, it will not be written at all.

for rule in "${!rules[@]}"; do
  regex="s/#\?\(${rule}\s*\).*$/\1 ${rules[${rule}]}/"
  sed "${regex}" ./data.txt > temp.txt;
  mv -f temp.txt ./data.txt
done

Result:

LoginGraceTime  1m
PermitRootLogin  no
PermitRootLogin  no
PasswordAuthentication  no

AWK

AWK is more solid in this situation. It yields better control. It will read line by line and replace the value if it exists without changing commented rules. If a rule wasn't found at the end of the file, it will append this rule to the file. This is much more solid than the SED approach. We can be sure there will be not duplicate rule and all rules are defined.

for rule in "${!rules[@]}"; do
awk -v key="${rule}" -v val="${rules[${rule}]}" \
  '$1==key {foundLine=1; print key " " val} $1!=key{print $0} END{if(foundLine!=1) print key " " val}' \
  ./data.txt > sshd_config.tmp && mv sshd_config.tmp ./data.txt
done

Result:

LoginGraceTime 1m
#PermitRootLogin yes
PermitRootLogin no
PasswordAuthentication no
AllowUsers blue

Conclusion

AWK is clearly the better choice. It is more safe and can handle rules that are not in the file.

The Fool
  • 10,692
  • 4
  • 27
  • 50
2

You could use awk for this:

awk '$1=="PermitRootLogin"{foundLine=1; print "PermitRootLogin no"} $1!="PermitRootLogin"{print $0} END{if(foundLine!=1) print "PermitRootLogin no"}' sshd_config > sshd_config.tmp && mv sshd_config.tmp sshd_config

That goes through each line in the file, if the first element (seperated by awk's default delims) is "PermitRootLogin" then change it to "no" and capture that you found it. If the line doesn't contain "PermitRootLogin" then just write it out as-is. If you didn't find "PermitRootLogin" after all the lines are processed (END) then write that line.

#Remove cause had to edit 6 chars....

Jackfritt
  • 15
  • 6
JNevill
  • 42,519
  • 3
  • 32
  • 55
1

If you have GNU sed 4.2.2 you can use the following trick:

sed -z 's/PermitRootLogin yes\|$/PermitRootLogin no/' file

-z will read lines delimited by NUL (\0) so basically enabling slurp mode in sed

-i will do in place edit on your file, so remember to make a backup before running with it

The substitution is pretty straight forward:

Replaces PermitRootLogin yes with PermitRootLogin no and if not found append PermitRootLogin no to the end.

You could add word boundaries around the search: \<PermitRootLogin yes\>

Please note that this is omit a trailing newline if PermitRootLogin yes is not matched, since PermitRootLogin no will be inserted after the last newline.

Andreas Louv
  • 44,338
  • 13
  • 91
  • 116
0

@cyrus:

My current script looks like this:

#!/bin/bash

touch /tmp/sshd_config
while read -r line || [[ -n "$line" ]]; do
    if [ "$line" = "PermitRootLogin yes" ]; then
        match=1
        echo "PermitRootLogin no" >> /tmp/sshd_config
    else
        echo "$line" >> /tmp/sshd_config
    fi
done < /etc/ssh/sshd_config

if [ "$match" != "1" ]; then
    echo "PermitRootLogin no" >> /tmp/sshd_config
fi

It works, but it looks poor. I'd prefer a more \s+ 'ish style to catch "PermitRootLogin(manySpaces)yes" and "PermitRootLogin(tabTabTab)yes" lines. An approach using 'grep' would definitely be nicer.

To anybody else who has answered so far and mentioned sed and awk: There are three caveats in your proposals. 1: I, as I am not the maintainer of the distro, cannot guarantee that one or both will be installed. 2: I don't know if people who want to modify my scripts are sed and/or awk cracks. 3: I can guarantee that, except of the fact that sed and awk are magnificent, awesome, elegant tools to deal with such stuff, I have absolutely no knowledge when it comes to sed and awk. And yes, this is a humiliating gap.

========= Post scriptum...

After playing around a bit and finding one ugly caveat in the original script, here is my current version:

#!/bin/bash

INFILE=/etc/ssh/sshd_config
TMPFILE=/var/tmp/sshd_config

touch $TMPFILE

while read -r line || [[ -n "$line" ]]; do
    if [ `echo "$line" | grep -c -P "^\s*PermitRootLogin\s+"` = "1" ]; then
        match=1
        echo "PermitRootLogin no" >> $TMPFILE
    else
        echo "$line" >> $TMPFILE
    fi
done < $INFILE

if [ "$match" != "1" ]; then
    echo "" >> $TMPFILE
    echo "# Do not permit root to log in directly" >> $TMPFILE
    echo "PermitRootLogin no" >> $TMPFILE
fi

cp -f $TMPFILE $INFILE
sync

The difference to the old version is, at first sight, the change from the simple comparison to grep, but the pcre is indeed neccessary. If a future distro comes with "PermitRootLogin no", the former version will add an at least unneccessary entry to the config. Another nifty thing is that the config file, at another line, contains "PermitRootLogin yes" within a comment. A simple grep -c -P "PermitRootLogin\s+yes" would match there (again).

The new version still looks clumsy and ugly, but it works :)

CarstenP
  • 123
  • 1
  • 8
0

This can be further simplified as a few liner script where you pass variables to change and it can change on the fly for you. Check all sorts of syntax and make sure end results are all perfectly done.

yes and no can be converted into a switch as well

To User Scripts Do

replace_string.sh sshd_config.old PasswordAuthentication PermitRootLogin

My Script Looks Like this


#! /bin/bash 
sshd_config_file=$1

search_replace() 
{
grep -i "^${search_string} yes$" ${sshd_config_file} | grep -v "^#"|grep -v "${search_string} no" || sed -i "" "s|^#*${search_string}.*$|${search_string} yes|" ${sshd_config_file} 
}

#for search_string in $@; do 
for search_string in $(eval echo ${*:2}); do 
  search_replace
done