Created
September 5, 2017 10:14
-
-
Save vasuadari/7d02f6a3e8c84c6c5e8ad1cd5087124e to your computer and use it in GitHub Desktop.
Bootstraps bastion on Linux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash -e | |
# Bastion Bootstrapping | |
# NOTE: This requires GNU getopt. On Mac OS X and FreeBSD you must install GNU getopt and mod the checkos function so that it's supported | |
# Configuration | |
PROGRAM='Linux Bastion' | |
##################################### Functions Definitions | |
function checkos () { | |
platform='unknown' | |
unamestr=`uname` | |
if [[ "$unamestr" == 'Linux' ]]; then | |
platform='linux' | |
else | |
echo "[WARNING] This script is not supported on MacOS or freebsd" | |
exit 1 | |
fi | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function usage () { | |
echo "$0 <usage>" | |
echo " " | |
echo "options:" | |
echo -e "--help \t Show options for this script" | |
echo -e "--banner \t Enable or Disable Bastion Message" | |
echo -e "--enable \t SSH Banner" | |
echo -e "--tcp-forwarding \t Enable or Disable TCP Forwarding" | |
echo -e "--x11-forwarding \t Enable or Disable X11 Forwarding" | |
} | |
function chkstatus () { | |
if [ $? -eq 0 ] | |
then | |
echo "Script [PASS]" | |
else | |
echo "Script [FAILED]" >&2 | |
exit 1 | |
fi | |
} | |
function osrelease () { | |
OS=`cat /etc/os-release | grep '^NAME=' | tr -d \" | sed 's/\n//g' | sed 's/NAME=//g'` | |
if [ "$OS" == "Ubuntu" ]; then | |
echo "Ubuntu" | |
elif [ "$OS" == "Amazon Linux AMI" ]; then | |
echo "AMZN" | |
elif [ "$OS" == "CentOS Linux" ]; then | |
echo "CentOS" | |
else | |
echo "Operating System Not Found" | |
fi | |
echo "${FUNCNAME[0]} Ended" >> /var/log/cfn-init.log | |
} | |
function harden_ssh_security () { | |
# Allow ec2-user only to access this folder and its content | |
#chmod -R 770 /var/log/bastion | |
#setfacl -Rdm other:0 /var/log/bastion | |
# Make OpenSSH execute a custom script on logins | |
echo -e "\nForceCommand /usr/bin/bastion/shell" >> /etc/ssh/sshd_config | |
# LOGGING CONFIGURATION | |
mkdir -p /var/log/bastion | |
mkdir -p /usr/bin/bastion | |
touch /tmp/messages | |
chmod 770 /tmp/messages | |
log_file_location="${bastion_mnt}/${bastion_log}" | |
log_shadow_file_location="${bastion_mnt}/.${bastion_log}" | |
cat <<'EOF' >> /usr/bin/bastion/shell | |
bastion_mnt="/var/log/bastion" | |
bastion_log="bastion.log" | |
# Check that the SSH client did not supply a command. Only SSH to instance should be allowed. | |
export Allow_SSH="ssh" | |
export Allow_SCP="scp" | |
if [[ -z $SSH_ORIGINAL_COMMAND ]] || [[ $SSH_ORIGINAL_COMMAND =~ ^$Allow_SSH ]] || [[ $SSH_ORIGINAL_COMMAND =~ ^$Allow_SCP ]]; then | |
#Allow ssh to instance and log connection | |
if [ -z "$SSH_ORIGINAL_COMMAND" ]; then | |
/bin/bash | |
exit 0 | |
else | |
$SSH_ORIGINAL_COMMAND | |
fi | |
log_file=`echo "$log_shadow_file_location"` | |
DATE_TIME_WHOAMI="`whoami`:`date "+%Y-%m-%d %H:%M:%S"`" | |
LOG_ORIGINAL_COMMAND=`echo "$DATE_TIME_WHOAMI:$SSH_ORIGINAL_COMMAND"` | |
echo "$LOG_ORIGINAL_COMMAND" >> "${bastion_mnt}/${bastion_log}" | |
log_dir="/var/log/bastion/" | |
else | |
# The "script" program could be circumvented with some commands | |
# (e.g. bash, nc). Therefore, I intentionally prevent users | |
# from supplying commands. | |
echo "This bastion supports interactive sessions only. Do not supply a command" | |
exit 1 | |
fi | |
EOF | |
# Make the custom script executable | |
chmod a+x /usr/bin/bastion/shell | |
release=$(osrelease) | |
if [ "$release" == "CentOS" ]; then | |
semanage fcontext -a -t ssh_exec_t /usr/bin/bastion/shell | |
fi | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function amazon_os () { | |
echo "${FUNCNAME[0]} Started" | |
chown root:ec2-user /usr/bin/script | |
service sshd restart | |
echo -e "\nDefaults env_keep += \"SSH_CLIENT\"" >>/etc/sudoers | |
cat <<'EOF' >> /etc/bashrc | |
#Added by linux bastion bootstrap | |
declare -rx IP=$(echo $SSH_CLIENT | awk '{print $1}') | |
EOF | |
echo " declare -rx BASTION_LOG=${BASTION_MNT}/${BASTION_LOG}" >> /etc/bashrc | |
cat <<'EOF' >> /etc/bashrc | |
declare -rx PROMPT_COMMAND='history -a >(logger -t "ON: $(date) [FROM]:${IP} [USER]:${USER} [PWD]:${PWD}" -s 2>>${BASTION_LOG})' | |
EOF | |
chown root:ec2-user ${BASTION_MNT} | |
chown root:ec2-user ${BASTION_LOGFILE} | |
chown root:ec2-user ${BASTION_LOGFILE_SHADOW} | |
chmod 662 ${BASTION_LOGFILE} | |
chmod 662 ${BASTION_LOGFILE_SHADOW} | |
chattr +a ${BASTION_LOGFILE} | |
chattr +a ${BASTION_LOGFILE_SHADOW} | |
touch /tmp/messages | |
chown root:ec2-user /tmp/messages | |
#Install CloudWatch Log service on AMZN | |
yum update -y | |
yum install -y awslogs | |
export CWG=`curl http://169.254.169.254/latest/user-data/ | grep CLOUDWATCHGROUP | sed 's/CLOUDWATCHGROUP=//g'` | |
echo "file = $BASTION_LOGFILE_SHADOW" >> /tmp/groupname.txt | |
echo "log_group_name = $CWG" >> /tmp/groupname.txt | |
cat <<'EOF' >> ~/cloudwatchlog.conf | |
[/var/log/bastion] | |
datetime_format = %b %d %H:%M:%S | |
buffer_duration = 5000 | |
log_stream_name = {instance_id} | |
initial_position = start_of_file | |
EOF | |
LINE=$(cat -n /etc/awslogs/awslogs.conf | grep '\[\/var\/log\/messages\]' | awk {'print $1'}) | |
END_LINE=$(echo $(($LINE-1))) | |
head -$END_LINE /etc/awslogs/awslogs.conf > /tmp/awslogs.conf | |
cat /tmp/awslogs.conf > /etc/awslogs/awslogs.conf | |
cat ~/cloudwatchlog.conf >> /etc/awslogs/awslogs.conf | |
cat /tmp/groupname.txt >> /etc/awslogs/awslogs.conf | |
export TMPREGION=`cat /etc/awslogs/awscli.conf | grep region` | |
export Region=`curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev` | |
sed -i.back "s/$TMPREGION/region = $Region/g" /etc/awslogs/awscli.conf | |
#Restart awslogs service | |
service awslogs restart | |
chkconfig awslogs on | |
#Run security updates | |
cat <<'EOF' >> ~/mycron | |
0 0 * * * yum -y update --security | |
EOF | |
crontab ~/mycron | |
rm ~/mycron | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function ubuntu_os () { | |
chown syslog:adm /var/log/bastion | |
chown root:ubuntu /usr/bin/script | |
cat <<'EOF' >> /etc/bash.bashrc | |
#Added by linux bastion bootstrap | |
declare -rx IP=$(who am i --ips|awk '{print $5}') | |
EOF | |
echo " declare -rx BASTION_LOG=${BASTION_MNT}/${BASTION_LOG}" >> /etc/bash.bashrc | |
cat <<'EOF' >> /etc/bash.bashrc | |
declare -rx PROMPT_COMMAND='history -a >(logger -t "ON: $(date) [FROM]:${IP} [USER]:${USER} [PWD]:${PWD}" -s 2>>${BASTION_LOG})' | |
EOF | |
chown root:ubuntu ${BASTION_MNT} | |
chown root:ubuntu ${BASTION_LOGFILE} | |
chown root:ubuntu ${BASTION_LOGFILE_SHADOW} | |
chmod 662 ${BASTION_LOGFILE} | |
chmod 662 ${BASTION_LOGFILE_SHADOW} | |
chattr +a ${BASTION_LOGFILE} | |
chattr +a ${BASTION_LOGFILE_SHADOW} | |
touch /tmp/messages | |
chown root:ubuntu /tmp/messages | |
#Restart SSH | |
service ssh stop | |
service ssh start | |
#Run security updates | |
apt-get install unattended-upgrades | |
cat <<'EOF' >> ~/mycron | |
0 0 * * * unattended-upgrades -d | |
EOF | |
crontab ~/mycron | |
rm ~/mycron | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function cent_os () { | |
echo -e "\nDefaults env_keep += \"SSH_CLIENT\"" >>/etc/sudoers | |
cat <<'EOF' >> /etc/bashrc | |
#Added by linux bastion bootstrap | |
declare -rx IP=$(echo $SSH_CLIENT | awk '{print $1}') | |
EOF | |
echo "declare -rx BASTION_LOG=${BASTION_MNT}/${BASTION_LOG}" >> /etc/bashrc | |
cat <<'EOF' >> /etc/bashrc | |
declare -rx PROMPT_COMMAND='history -a >(logger -t "ON: $(date) [FROM]:${IP} [USER]:${USER} [PWD]:${PWD}" -s 2>>${BASTION_LOG})' | |
EOF | |
chown root:centos ${BASTION_MNT} | |
chown root:centos /usr/bin/script | |
chown root:centos /var/log/bastion/bastion.log | |
chmod 770 /var/log/bastion/bastion.log | |
touch /tmp/messages | |
chown root:centos /tmp/messages | |
restorecon -v /etc/ssh/sshd_config | |
/bin/systemctl restart sshd.service | |
# Install CloudWatch Log service on Centos Linux | |
export CWG=`curl http://169.254.169.254/latest/user-data/ | grep CLOUDWATCHGROUP | sed 's/CLOUDWATCHGROUP=//g'` | |
centos=`cat /etc/os-release | grep VERSION_ID | tr -d \VERSION_ID=\"` | |
if [ "$centos" == "7" ]; then | |
echo "file = $BASTION_LOGFILE_SHADOW" >> /tmp/groupname.txt | |
echo "log_group_name = $CWG" >> /tmp/groupname.txt | |
cat <<'EOF' >> ~/cloudwatchlog.conf | |
[general] | |
state_file = /var/awslogs/state/agent-state | |
use_gzip_http_content_encoding = true | |
logging_config_file = /var/awslogs/etc/awslogs.conf | |
[/var/log/bastion] | |
datetime_format = %Y-%m-%d %H:%M:%S | |
file = /var/log/messages | |
buffer_duration = 5000 | |
log_stream_name = {instance_id} | |
initial_position = start_of_file | |
EOF | |
export Region=`curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev` | |
cat /tmp/groupname.txt >> ~/cloudwatchlog.conf | |
curl https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py -O | |
chmod +x ./awslogs-agent-setup.py | |
./awslogs-agent-setup.py -n -r $Region -c ~/cloudwatchlog.conf | |
cat <<'EOF' >> /etc/systemd/system/awslogs.service | |
[Unit] | |
Description=The CloudWatch Logs agent | |
After=rc-local.service | |
[Service] | |
Type=simple | |
Restart=always | |
KillMode=process | |
TimeoutSec=infinity | |
PIDFile=/var/awslogs/state/awslogs.pid | |
ExecStart=/var/awslogs/bin/awslogs-agent-launcher.sh --start --background --pidfile $PIDFILE --user awslogs --chuid awslogs & | |
[Install] | |
WantedBy=multi-user.target | |
EOF | |
service awslogs restart | |
chkconfig awslogs on | |
else | |
chown root:centos /var/log/bastion | |
yum update -y | |
yum install -y awslogs | |
export Region=`curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev` | |
export TMPREGION=`cat /etc/awslogs/awscli.conf | grep region` | |
sed -i.back "s/$TMPREGION/region = $Region/g" /etc/awslogs/awscli.conf | |
export CWG=`curl http://169.254.169.254/latest/user-data/ | grep CLOUDWATCHGROUP | sed 's/CLOUDWATCHGROUP=//g'` | |
echo "file = $BASTION_LOGFILE_SHADOW" >> /tmp/groupname.txt | |
echo "log_group_name = $CWG" >> /tmp/groupname.txt | |
cat <<'EOF' >> ~/cloudwatchlog.conf | |
[/var/log/bastion] | |
datetime_format = %b %d %H:%M:%S | |
buffer_duration = 5000 | |
log_stream_name = {instance_id} | |
initial_position = start_of_file | |
EOF | |
export TMPGROUP=`cat /etc/awslogs/awslogs.conf | grep ^log_group_name` | |
export TMPGROUP=`echo $TMPGROUP | sed 's/\//\\\\\//g'` | |
sed -i.back "s/$TMPGROUP/log_group_name = $CWG/g" /etc/awslogs/awslogs.conf | |
cat ~/cloudwatchlog.conf >> /etc/awslogs/awslogs.conf | |
cat /tmp/groupname.txt >> /etc/awslogs/awslogs.conf | |
yum install ec2-metadata -y | |
export TMPREGION=`cat /etc/awslogs/awscli.conf | grep region` | |
export Region=`curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev` | |
sed -i.back "s/$TMPREGION/region = $Region/g" /etc/awslogs/awscli.conf | |
sleep 3 | |
service awslogs stop | |
sleep 3 | |
service awslogs start | |
chkconfig awslogs on | |
fi | |
#Run security updates | |
cat <<'EOF' >> ~/mycron | |
0 0 * * * yum -y update --security | |
EOF | |
crontab ~/mycron | |
rm ~/mycron | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function request_eip() { | |
release=$(osrelease) | |
export Region=`curl http://169.254.169.254/latest/meta-data/placement/availability-zone | rev | cut -c 2- | rev` | |
#Check if EIP already assigned. | |
ALLOC=1 | |
ZERO=0 | |
INSTANCE_IP=`ifconfig -a | grep inet | awk {'print $2'} | sed 's/addr://g' | head -1` | |
ASSIGNED=$(aws ec2 describe-addresses --region $Region --output text | grep $INSTANCE_IP | wc -l) | |
if [ "$ASSIGNED" -gt "$ZERO" ]; then | |
echo "Already assigned an EIP." | |
else | |
aws ec2 describe-addresses --region $Region --output text > /query.txt | |
#Ensure we are only using EIPs from our Stack | |
line=`curl http://169.254.169.254/latest/user-data/ | grep EIP_LIST` | |
IFS=$':' DIRS=(${line//$','/:}) # Replace tabs with colons. | |
for (( i=0 ; i<${#DIRS[@]} ; i++ )); do | |
EIP=`echo ${DIRS[i]} | sed 's/\"//g' | sed 's/EIP_LIST=//g'` | |
if [ $EIP != "Null" ]; then | |
#echo "$i: $EIP" | |
grep "$EIP" /query.txt >> /query2.txt; | |
fi | |
done | |
mv /query2.txt /query.txt | |
AVAILABLE_EIPs=`cat /query.txt | wc -l` | |
if [ "$AVAILABLE_EIPs" -gt "$ZERO" ]; then | |
FIELD_COUNT="5" | |
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id) | |
echo "Running associate_eip_now" | |
while read name; | |
do | |
#EIP_ENTRY=$(echo $name | grep eip | wc -l) | |
EIP_ENTRY=$(echo $name | grep eni | wc -l) | |
echo "EIP: $EIP_ENTRY" | |
if [ "$EIP_ENTRY" -eq 1 ]; then | |
echo "Already associated with an instance" | |
echo "" | |
else | |
export EIP=`echo "$name" | sed 's/[\s]+/,/g' | awk {'print $4'}` | |
EIPALLOC=`echo $name | awk {'print $2'}` | |
echo "NAME: $name" | |
echo "EIP: $EIP" | |
echo "EIPALLOC: $EIPALLOC" | |
aws ec2 associate-address --instance-id $INSTANCE_ID --allocation-id $EIPALLOC --region $Region | |
fi | |
done < /query.txt | |
else | |
echo "[ERROR] No Elastic IPs available in this region" | |
exit 1 | |
fi | |
INSTANCE_IP=`ifconfig -a | grep inet | awk {'print $2'} | sed 's/addr://g' | head -1` | |
ASSIGNED=$(aws ec2 describe-addresses --region $Region --output text | grep $INSTANCE_IP | wc -l) | |
if [ "$ASSIGNED" -eq 1 ]; then | |
echo "EIP successfully assigned." | |
else | |
#Retry | |
while [ "$ASSIGNED" -eq "$ZERO" ] | |
do | |
sleep 3 | |
request_eip | |
INSTANCE_IP=`ifconfig -a | grep inet | awk {'print $2'} | sed 's/addr://g' | head -1` | |
ASSIGNED=$(aws ec2 describe-addresses --region $Region --output text | grep $INSTANCE_IP | wc -l) | |
done | |
fi | |
fi | |
echo "${FUNCNAME[0]} Ended" | |
} | |
function prevent_process_snooping() { | |
# Prevent bastion host users from viewing processes owned by other users. | |
mount -o remount,rw,hidepid=2 /proc | |
awk '!/proc/' /etc/fstab > temp && mv temp /etc/fstab | |
echo "proc /proc proc defaults,hidepid=2 0 0" >> /etc/fstab | |
echo "${FUNCNAME[0]} Ended" | |
} | |
##################################### End Function Definitions | |
# Call checkos to ensure platform is Linux | |
checkos | |
## set an initial value | |
SSH_BANNER="LINUX BASTION" | |
# Read the options from cli input | |
TEMP=`getopt -o h: --long help,banner:,enable:,tcp-forwarding:,x11-forwarding: -n $0 -- "$@"` | |
eval set -- "$TEMP" | |
if [ $# == 1 ] ; then echo "No input provided! type ($0 --help) to see usage help" >&2 ; exit 1 ; fi | |
# extract options and their arguments into variables. | |
while true; do | |
case "$1" in | |
-h | --help) | |
usage | |
exit 1 | |
;; | |
--banner) | |
BANNER_PATH="$2"; | |
shift 2 | |
;; | |
--enable) | |
ENABLE="$2"; | |
shift 2 | |
;; | |
--tcp-forwarding) | |
TCP_FORWARDING="$2"; | |
shift 2 | |
;; | |
--x11-forwarding) | |
X11_FORWARDING="$2"; | |
shift 2 | |
;; | |
--) | |
break | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
# BANNER CONFIGURATION | |
BANNER_FILE="/etc/ssh_banner" | |
if [[ $ENABLE == "true" ]];then | |
if [ -z ${BANNER_PATH} ];then | |
echo "BANNER_PATH is null skipping ..." | |
else | |
echo "BANNER_PATH = ${BANNER_PATH}" | |
echo "Creating Banner in ${BANNER_FILE}" | |
echo "curl -s ${BANNER_PATH} > ${BANNER_FILE}" | |
curl -s ${BANNER_PATH} > ${BANNER_FILE} | |
if [ $BANNER_FILE ] ;then | |
echo "[INFO] Installing banner ... " | |
echo -e "\n Banner ${BANNER_FILE}" >>/etc/ssh/sshd_config | |
else | |
echo "[INFO] banner file is not accessible skipping ..." | |
exit 1; | |
fi | |
fi | |
else | |
echo "Banner message is not enabled!" | |
fi | |
# LOGGING CONFIGURATION | |
declare -rx BASTION_MNT="/var/log/bastion" | |
declare -rx BASTION_LOG="bastion.log" | |
echo "Setting up bastion session log in ${BASTION_MNT}/${BASTION_LOG}" | |
mkdir -p ${BASTION_MNT} | |
declare -rx BASTION_LOGFILE="${BASTION_MNT}/${BASTION_LOG}" | |
declare -rx BASTION_LOGFILE_SHADOW="${BASTION_MNT}/.${BASTION_LOG}" | |
touch ${BASTION_LOGFILE} | |
# ln ${BASTION_LOGFILE} ${BASTION_LOGFILE_SHADOW} | |
#Enable/Disable TCP forwarding | |
TCP_FORWARDING=`echo "$TCP_FORWARDING" | sed 's/\\n//g'` | |
#Enable/Disable X11 forwarding | |
X11_FORWARDING=`echo "$X11_FORWARDING" | sed 's/\\n//g'` | |
echo "Value of TCP_FORWARDING - $TCP_FORWARDING" | |
echo "Value of X11_FORWARDING - $X11_FORWARDING" | |
if [[ $TCP_FORWARDING == "false" ]];then | |
awk '!/AllowTcpForwarding/' /etc/ssh/sshd_config > temp && mv temp /etc/ssh/sshd_config | |
echo "AllowTcpForwarding no" >> /etc/ssh/sshd_config | |
harden_ssh_security | |
fi | |
if [[ $X11_FORWARDING == "false" ]];then | |
awk '!/X11Forwarding/' /etc/ssh/sshd_config > temp && mv temp /etc/ssh/sshd_config | |
echo "X11Forwarding no" >> /etc/ssh/sshd_config | |
fi | |
release=$(osrelease) | |
# Ubuntu Linux | |
if [ "$release" == "Ubuntu" ]; then | |
#Call function for Ubuntu | |
ubuntu_os | |
# AMZN Linux | |
elif [ "$release" == "AMZN" ]; then | |
#Call function for AMZN | |
amazon_os | |
# CentOS Linux | |
elif [ "$release" == "CentOS" ]; then | |
#Call function for CentOS | |
cent_os | |
else | |
echo "[ERROR] Unsupported Linux Bastion OS" | |
exit 1 | |
fi | |
prevent_process_snooping | |
echo "Bootstrap complete." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment