#!/bin/bash ### A fairly minimal, reasonably secure foundation for whatever you might be doing on a debian-based linode. ### Mostly this just automates ### 1. https://www.linode.com/docs/getting-started/ ### 2. https://www.linode.com/docs/security/securing-your-server/ ### This StackScript assumes you have previously (on the linode website) created a working linode with known ### * IP# (needed below) ### * root password ### * distro ~= Debian ### This StackScript depends/source's the the StackScript Bash Library [ https://www.linode.com/stackscripts/view/1 ] ### ---------------------------------------------------------------------- ### constants ### ---------------------------------------------------------------------- ### UDFs: variables you define through the StackScript GUI # # # # # # # # ### non-UDFs: locally-defined or -derived ## just for logging THIS_FN='minimal_secure_debian-based_linode' MESSAGE_PREFIX="${THIS_FN}:" WARNING_PREFIX="${MESSAGE_PREFIX} WARNING:" ERROR_PREFIX="${MESSAGE_PREFIX} ERROR:" ## user data # remember to set USER_PASSWORD! if [[ -z "${USER_PASSWORD}" ]] ; then echo -e "${ERROR_PREFIX} USER_PASSWORD not defined, exiting ..." exit 2 fi USER_HOME_DIR="/home/${USER_NAME}" # TODO: get from `usermod` or other USER_SSH_DIR="${USER_HOME_DIR}/.ssh" USER_KEYS_FP="${USER_SSH_DIR}/authorized_keys" ## packages to install MINIMAL_PACKAGE_LIST='sudo fail2ban' # always install these MY_PACKAGE_LIST="${MINIMAL_PACKAGE_LIST} less wget ${MY_EDITOR_PKG_NAMES}" ## box data MY_FQDN="${MY_HOSTNAME}.linode.com" MY_IPV4_IN_HOSTS_FILE="${MY_IPV4} ${MY_FQDN} ${MY_HOSTNAME}" MY_IPV6_IN_HOSTS_FILE="${MY_IPV6} ${MY_FQDN} ${MY_HOSTNAME}" ## firewall # firewall rules. copy/mod from https://www.linode.com/docs/security/securing-your-server/ # TODO: parameterize SSH port# # TODO: fix addition of comments: gotta replace "template '%' with '#' MY_FIREWALL_RULES='*filter %%% Contents (including line above) copy/mod from https://www.linode.com/docs/security/securing-your-server/ %%% Allow all loopback (lo0) traffic and drop all traffic to 127/8 that does not use lo0. -A INPUT -i lo -j ACCEPT -A INPUT -d 127.0.0.0/8 -j REJECT %%% Accept all established inbound connections. -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT %%% Allow all outbound traffic. TODO: modify this to only allow certain traffic -A OUTPUT -j ACCEPT %%% Allow HTTP and HTTPS connections from anywhere (the normal ports for websites and SSL). -A INPUT -p tcp --dport 80 -j ACCEPT -A INPUT -p tcp --dport 443 -j ACCEPT %%% Allow SSH connections. %%% note -dport port% must match port% set in sshd_config -A INPUT -p tcp -m state --state NEW --dport 22 -j ACCEPT %%% Allow ping -A INPUT -p icmp -j ACCEPT %%% Log iptables denied calls -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7 %%% Drop all other inbound: policy=default deny unless explicitly allowed -A INPUT -j DROP -A FORWARD -j DROP COMMIT ' # end MY_FIREWALL_RULES MY_FIREWALL_RULES="$(echo -e "${MY_FIREWALL_RULES}" | sed -e 's/%/#/g')" # firewall start script lines, to go in /etc/network/if-pre-up.d/firewall # TODO: fix addition of comments MY_FIREWALL_SCRIPT_LINES='%!/bin/sh /sbin/iptables-restore < /etc/iptables.firewall.rules ' MY_FIREWALL_SCRIPT_LINES="$(echo -e "${MY_FIREWALL_SCRIPT_LINES}" | sed -e 's/%/#/g')" ### ---------------------------------------------------------------------- ### functions ### ---------------------------------------------------------------------- ## include StackScript Bash Library: see ## * https://www.linode.com/docs/platform/stackscripts/ ## * https://www.linode.com/stackscripts/view/1 source ## Copy/mod of function=goodstuff from StackScript Bash Library (which this overrides) function goodstuff { if [[ -z "${MY_PACKAGE_LIST}" ]] ; then aptitude -y install ${MINIMAL_PACKAGE_LIST} else aptitude -y install ${MY_PACKAGE_LIST} fi # enable color root prompt sed -i -e 's/^#PS1=/PS1=/' /root/.bashrc # enable the colorful root bash prompt sed -i -e "s/^#alias ll='ls -l'/alias ll='ls -al'/" /root/.bashrc # enable ll list long alias } ## Copy/mod of function=user_add_pubkey from StackScript Bash Library (which this overrides). ## Adds the user's public key to authorized_keys for the specified user. (This adds `chmod` of SSH dir and keys file ... and improves bash style, IMHO.) ## Quote inputs, or the key may not load properly. function user_add_pubkey { # $1 - Required - username # $2 - Required - public key local USERNAME="${1}" local USERPUBKEY="${2}" local USERSSHDIR='' if [[ -z "${USERNAME}" || -z "${USERPUBKEY}" ]] ; then echo -e "${ERROR_PREFIX} must provide a username and a public SSH key" exit 5 elif [[ "${USERNAME}" == 'root' ]] ; then USERSSHDIR='/root/.ssh' else USERSSHDIR="/home/${USERNAME}/.ssh" fi local USERKEYSFILE="${USERSSHDIR}/authorized_keys" mkdir -p "${USERSSHDIR}/" echo -e "${USERPUBKEY}" >> "${USERKEYSFILE}" if [[ "${USERNAME}" != 'root' ]] ; then chown -R "${USERNAME}":"${USERNAME}" "${USERSSHDIR}" chmod 0700 "${USERSSHDIR}" chmod 0600 "${USERKEYSFILE}" fi } ## A quick'n'dirty way to "backup" files. ## TODO: test if current user lacks sufficient privileges for file to backup. function backup { local BACKUP_FP="${1}" # file to backup local BACKED_UP_FP="${BACKUP_FP}.0" if [[ -z "${BACKUP_FP}" ]] ; then echo -e "${ERROR_PREFIX} must provide path to file to backup" return 1; # use `return` for StackScript Bash Library, `exit` for normal scripts elif [[ ! -r "${BACKUP_FP}" ]] ; then echo -e "${WARNING_PREFIX} nop: cannot read file to backup='${BACKUP_FP}'" elif [[ -r "${BACKED_UP_FP}" ]] ; then echo -e "${WARNING_PREFIX} nop: previous backup='${BACKED_UP_FP}' exists" else cp ${BACKUP_FP} ${BACKED_UP_FP} chmod a-w ${BACKED_UP_FP} fi } ### ---------------------------------------------------------------------- ### payload ### ---------------------------------------------------------------------- ## set hostname. uses StackScript Bash Library::system_set_hostname. TODO: test /etc/hostname for CMD in \ "backup /etc/hostname" \ "system_set_hostname '${MY_HOSTNAME}'" \ "cat /etc/hostname" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done if [[ -r '/etc/default/dhcpcd' ]] ; then SET_HOSTNAME_LINES="$(fgrep -e 'SET_HOSTNAME' /etc/default/dhcpcd)" if [[ -n "${SET_HOSTNAME_LINES}" ]] ; then echo -e "${WARNING_PREFIX} TODO: comment out line(s) setting token='SET_HOSTNAME' in file='/etc/default/dhcpcd'" fi fi echo # newline ## set IP. TODO: test IP#s, /etc/hosts for CMD in \ "backup /etc/hosts" \ "echo >> /etc/hosts" \ "echo -e '# added by ${THIS_FN}' >> /etc/hosts" \ "echo -e '${MY_IPV4_IN_HOSTS_FILE}' >> /etc/hosts" \ "echo -e '${MY_IPV6_IN_HOSTS_FILE}' >> /etc/hosts" \ "cat /etc/hosts" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline ## set timezone as desired: this replaces `dpkg-reconfigure tzdata` for CMD in \ "backup /etc/timezone" \ "echo '${MY_TZ_LOCATION}' > /etc/timezone" \ "dpkg-reconfigure -f noninteractive tzdata" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline ## add new user, after ensuring package=`sudo` is installed. ## Note `chpasswd` is the batch-mode/non-interactive `passwd` ## TODO: test input strings and paths, (output) results ## TODO: replace with code from StackScript Bash Library::??? for CMD in \ "aptitude -y install sudo" \ "adduser ${USER_NAME} --disabled-password --gecos ''" \ "echo '${USER_NAME}:${USER_PASSWORD}' | chpasswd" \ "usermod -aG sudo ${USER_NAME}" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline if [[ ! -w "${USER_HOME_DIR}" ]] ; then echo -e "${ERROR_PREFIX} cannot write to home dir='${USER_HOME_DIR}' for new user='${USER_NAME}', exiting ..." else # setup user's SSH with this::user_add_pubkey (extends StackScript Bash Library::user_add_pubkey) # disable SSH login as user=root with StackScript Bash Library::ssh_disable_root for CMD in \ "user_add_pubkey '${USER_NAME}' '${USER_PUBLIC_SSHKEY}'" \ "ls -al ${USER_KEYS_FP}" \ "ls -ald ${USER_SSH_DIR}" \ "ssh_disable_root" \ "fgrep -nH -e 'PermitRootLogin' /etc/ssh/sshd_config" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done fi echo # newline ## setup firewall echo -e "${MESSAGE_PREFIX} initial iptables set=" for CMD in \ "iptables -L" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done # "cat /etc/iptables.firewall.rules" \ # superfluous for CMD in \ "backup /etc/iptables.firewall.rules" \ "backup /etc/network/if-pre-up.d/firewall" \ "echo -e '${MY_FIREWALL_RULES}' > /etc/iptables.firewall.rules" \ "iptables-restore < /etc/iptables.firewall.rules" \ "iptables -L" \ "echo -e '${MY_FIREWALL_SCRIPT_LINES}' > /etc/network/if-pre-up.d/firewall" \ "chmod +x /etc/network/if-pre-up.d/firewall" \ "ls -al /etc/network/if-pre-up.d/firewall" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline ## install packages: ## `system_update` from StackScript Bash Library ## `goodstuff` above overrides implementation in StackScript Bash Library for CMD in \ "system_update" \ "goodstuff" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline ## configure Fail2Ban? no, take defaults. see https://www.linode.com/docs/security/securing-your-server/ ## done! restartServices # StackScript Bash Library::restartServices echo -e " ${THIS_FN}: complete! Now, logout of this SSH session (as root), and verify that 1. you *cannot* now SSH in as user=root 2. you *can* now SSH in as user='${USER_NAME}' with key and without challenge 3. your 'sudo iptables -L' output 3.1. resembles listing @ https://www.linode.com/docs/security/securing-your-server/#creating-a-firewall 3.2. ... and your ability to 'sudo' tests that you set your user password correctly. 4. your desired packages (esp for your editor) are installed "