#!/usr/bin/env 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: constants you define through the StackScript GUI ### ---------------------------------------------------------------------- # # # # # # # # # ### ---------------------------------------------------------------------- ### non-UDFs: locally-defined or -derived ### ---------------------------------------------------------------------- ### just for logging THIS_FN='minimal_secure_debian-based_linode' # who we are MESSAGE_PREFIX="${THIS_FN}:" WARNING_PREFIX="${MESSAGE_PREFIX} WARNING:" ERROR_PREFIX="${MESSAGE_PREFIX} ERROR:" ### private properties (corresponding to private.properties in ../scripts/) if [[ -z "${LINODE_USER_PASSWORD}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_USER_PASSWORD not defined, exiting ..." exit 4 elif [[ -z "${LINODE_USER_NAME}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_USER_NAME not defined, exiting ..." exit 5 elif [[ -z "${LINODE_USER_PUBLIC_SSHKEY}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_USER_PUBLIC_SSHKEY not defined, exiting ..." exit 6 elif [[ -z "${LINODE_HOSTNAME}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_HOSTNAME not defined, exiting ..." exit 7 elif [[ -z "${LINODE_IPV4}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_IPV4 not defined, exiting ..." exit 8 elif [[ -z "${LINODE_IPV6}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_IPV6 not defined, exiting ..." exit 9 # can `source private.properties`, but can't compose UDFs # elif [[ -z "${LINODE_FQDN}" ]] ; then # echo -e "${ERROR_PREFIX} LINODE_FQDN not defined, exiting ..." # exit 10 elif [[ -z "${LINODE_TZ_LOCATION}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_TZ_LOCATION not defined, exiting ..." exit 11 fi ## derived from UDFs LINODE_FQDN="${LINODE_HOSTNAME}.linode.com" LINODE_USER_HOME_DIR="/home/${LINODE_USER_NAME}" # TODO: get from `usermod` or other LINODE_USER_SSH_DIR="${LINODE_USER_HOME_DIR}/.ssh" LINODE_USER_KEYS_FP="${LINODE_USER_SSH_DIR}/authorized_keys" LINODE_IPV4_IN_HOSTS_FILE="${LINODE_IPV4} ${LINODE_FQDN} ${LINODE_HOSTNAME}" LINODE_IPV6_IN_HOSTS_FILE="${LINODE_IPV6} ${LINODE_FQDN} ${LINODE_HOSTNAME}" ### public properties (corresponding to public.properties in ../scripts/ ## packages to install LINODE_PACKAGE_LIST="${MINIMAL_PACKAGE_LIST} ${LINODE_EDITOR_PKG_LIST}" ## 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 '#' LINODE_FIREWALL_RULES_FP='/etc/iptables.firewall.rules' # path to rules file LINODE_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 LINODE_FIREWALL_RULES LINODE_FIREWALL_RULES="$(echo -e "${LINODE_FIREWALL_RULES}" | sed -e 's/%/#/g')" # firewall start script lines # TODO: fix addition of comments LINODE_FIREWALL_SCRIPT_FP='/etc/network/if-pre-up.d/firewall' LINODE_FIREWALL_SCRIPT_LINES="%!/bin/sh /sbin/iptables-restore < ${LINODE_FIREWALL_RULES_FP} " LINODE_FIREWALL_SCRIPT_LINES="$(echo -e "${LINODE_FIREWALL_SCRIPT_LINES}" | sed -e 's/%/#/g')" if [[ -z "${LINODE_PACKAGE_LIST}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_PACKAGE_LIST not defined, exiting ..." exit 14 elif [[ -z "${LINODE_FIREWALL_RULES}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_FIREWALL_RULES not defined, exiting ..." exit 15 elif [[ -z "${LINODE_FIREWALL_SCRIPT_LINES}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_FIREWALL_SCRIPT_LINES not defined, exiting ..." exit 16 elif [[ -z "${LINODE_FIREWALL_RULES_FP}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_FIREWALL_RULES_FP not defined, exiting ..." exit 17 elif [[ -z "${LINODE_FIREWALL_SCRIPT_FP}" ]] ; then echo -e "${ERROR_PREFIX} LINODE_FIREWALL_SCRIPT_FP not defined, exiting ..." exit 18 elif [[ -z "${STACKSCRIPT_BASH_LIBRARY_FN}" ]] ; then echo -e "${ERROR_PREFIX} STACKSCRIPT_BASH_LIBRARY_FN not defined, exiting ..." exit 19 elif [[ -z "${MY_BASH_LIBRARY_FN}" ]] ; then echo -e "${ERROR_PREFIX} MY_BASH_LIBRARY_FN not defined, exiting ..." exit 20 # no: this will be created # elif [[ ! -r "${LINODE_FIREWALL_RULES_FP}" ]] ; then # echo -e "${ERROR_PREFIX} cannot read firewall rules file='${LINODE_FIREWALL_RULES_FP}', exiting ..." # exit 21 # this will also be created # elif [[ ! -r "${LINODE_FIREWALL_SCRIPT_FP}" ]] ; then # echo -e "${ERROR_PREFIX} cannot read firewall script file='${LINODE_FIREWALL_SCRIPT_FP}', exiting ..." # exit 22 fi ### ---------------------------------------------------------------------- ### functions ### ---------------------------------------------------------------------- ## include StackScript Bash Library: see ## * https://www.linode.com/docs/platform/stackscripts/ ## * https://www.linode.com/stackscripts/view/1 source ### TODO: provide following from another StackScript (à la ../scripts/my_linode_bash_library.sh) ## Copy/mod of function=goodstuff from StackScript Bash Library (which this overrides) function goodstuff { if [[ -z "${LINODE_PACKAGE_LIST}" ]] ; then aptitude -y install ${MINIMAL_PACKAGE_LIST} else aptitude -y install ${LINODE_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 '${LINODE_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 '${LINODE_IPV4_IN_HOSTS_FILE}' >> /etc/hosts" \ "echo -e '${LINODE_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 '${LINODE_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 ${LINODE_USER_NAME} --disabled-password --gecos ''" \ "echo '${LINODE_USER_NAME}:${LINODE_USER_PASSWORD}' | chpasswd" \ "usermod -aG sudo ${LINODE_USER_NAME}" \ ; do echo -e "${MESSAGE_PREFIX} ${CMD}" eval "${CMD}" done echo # newline if [[ ! -w "${LINODE_USER_HOME_DIR}" ]] ; then echo -e "${ERROR_PREFIX} cannot write to home dir='${LINODE_USER_HOME_DIR}' for new user='${LINODE_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 '${LINODE_USER_NAME}' '${LINODE_USER_PUBLIC_SSHKEY}'" \ "ls -al ${LINODE_USER_KEYS_FP}" \ "ls -ald ${LINODE_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 ${LINODE_FIREWALL_RULES_FP}" \ # superfluous for CMD in \ "backup ${LINODE_FIREWALL_RULES_FP}" \ "backup ${LINODE_FIREWALL_SCRIPT_FP}" \ "echo -e '${LINODE_FIREWALL_RULES}' > ${LINODE_FIREWALL_RULES_FP}" \ "iptables-restore < ${LINODE_FIREWALL_RULES_FP}" \ "iptables -L" \ "echo -e '${LINODE_FIREWALL_SCRIPT_LINES}' > ${LINODE_FIREWALL_SCRIPT_FP}" \ "chmod +x ${LINODE_FIREWALL_SCRIPT_FP}" \ "ls -al ${LINODE_FIREWALL_SCRIPT_FP}" \ ; 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, return to your client, and verify that 1. you *cannot* now SSH into your linode as user=root 2. you *can* now SSH into your linode as user='${LINODE_USER_NAME}', with key authentication and without password challenge 3. your desired packages (e.g., for your editor) are installed on your linode 4. your 'sudo iptables -L' output 4.1. resembles listing @ https://www.linode.com/docs/security/securing-your-server/#creating-a-firewall 4.2. ... and your ability to 'sudo' tests that you set your user password correctly. " exit 0