From 9ad96d7c1dc54e698de38640a6a64588724d9a07 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:20:08 -0400 Subject: [PATCH] Update Digital Ocean Marketplace files for Packer (#1923) --- util/DigitalOceanMarketplace/fabfile.py | 132 ---- .../files/etc/ufw/applications.d/bitwarden | 4 + .../files/opt/bitwarden/install-bitwarden.sh | 0 .../lib/cloud/scripts/per-instance/001_onboot | 10 +- .../marketplace-image.json | 86 +++ util/DigitalOceanMarketplace/packages.txt | 0 .../scripts/01-install-docker | 33 - ...-setup-first-run => 01-setup-first-run.sh} | 2 +- .../scripts/02-install-docker-compose | 18 - .../scripts/02-ufw-bitwarden.sh | 6 + .../scripts/03-force-ssh-logout.sh | 10 + .../scripts/90-cleanup.sh | 49 ++ .../scripts/99-img-check.sh | 617 ++++++++++++++++++ 13 files changed, 781 insertions(+), 186 deletions(-) delete mode 100644 util/DigitalOceanMarketplace/fabfile.py create mode 100644 util/DigitalOceanMarketplace/files/etc/ufw/applications.d/bitwarden mode change 100644 => 100755 util/DigitalOceanMarketplace/files/opt/bitwarden/install-bitwarden.sh mode change 100644 => 100755 util/DigitalOceanMarketplace/files/var/lib/cloud/scripts/per-instance/001_onboot create mode 100644 util/DigitalOceanMarketplace/marketplace-image.json delete mode 100644 util/DigitalOceanMarketplace/packages.txt delete mode 100644 util/DigitalOceanMarketplace/scripts/01-install-docker rename util/DigitalOceanMarketplace/scripts/{03-setup-first-run => 01-setup-first-run.sh} (80%) mode change 100644 => 100755 delete mode 100644 util/DigitalOceanMarketplace/scripts/02-install-docker-compose create mode 100755 util/DigitalOceanMarketplace/scripts/02-ufw-bitwarden.sh create mode 100755 util/DigitalOceanMarketplace/scripts/03-force-ssh-logout.sh create mode 100755 util/DigitalOceanMarketplace/scripts/90-cleanup.sh create mode 100755 util/DigitalOceanMarketplace/scripts/99-img-check.sh diff --git a/util/DigitalOceanMarketplace/fabfile.py b/util/DigitalOceanMarketplace/fabfile.py deleted file mode 100644 index 0e1bd49f7..000000000 --- a/util/DigitalOceanMarketplace/fabfile.py +++ /dev/null @@ -1,132 +0,0 @@ - #!/usr/bin/python -# -*- coding: utf-8 -*- - -from fabric.api import * -import os - -f = open("./packages.txt","r") -APT_PACKAGES = f.read() - -env.user = "root" - - -def clean_up(): - """ - Clean up remote machine before taking snapshot. - """ - run("apt-get -y update") - run("apt-get -y upgrade") - run("rm -rf /tmp/* /var/tmp/*") - run("history -c") - run("cat /dev/null > /root/.bash_history") - run("unset HISTFILE") - run("apt-get -y autoremove") - run("apt-get -y autoclean") - run("find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;") - run("rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????") - run("rm -rf /var/lib/cloud/instances/*") - run("rm -rf /var/lib/cloud/instance") - puts("Removing keys...") - run("rm -f /root/.ssh/authorized_keys /etc/ssh/*key*") - run("dd if=/dev/zero of=/zerofile; sync; rm /zerofile; sync") - run("cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp") - run("cat /dev/null > /var/log/auth.log") - - - -def install_files(): - """ - Install files onto remote machine. - Walk through the files in the "files" directory and copy them to the build system. - File permissions will be inherited. If you need to change permissions on uploaded files - you can do so in a script placed in the "scripts" directory. - """ - print "--------------------------------------------------" - print "Copying files in ./files to remote server" - print "--------------------------------------------------" - rootDir = './files' - for dirName, subdirList, fileList in os.walk(rootDir): - #print('Found directory: %s' % dirName) - cDir = dirName.replace("./files","") - print("Entering Directory: %s" % cDir) - if cDir: - run("mkdir -p %s" % cDir) - for fname in fileList: - cwd = os.getcwd() - rpath = cDir + "/" + fname - lpath = cwd + "/files" + cDir + "/" + fname - print('Moving File: %s' % lpath) - put(lpath,rpath,mirror_local_mode=True) - - - - -def install_pkgs(): - """ - Install apt packages listed in APT_PACKAGES - """ - #Postfix won't install without a prompt without setting some things - #run("debconf-set-selections <<< \"postfix postfix/main_mailer_type string 'No Configuration'\"") - #run("debconf-set-selections <<< \"postfix postfix/mailname string localhost.local\"") - run("DEBIAN_FRONTEND=noninteractive") - print "--------------------------------------------------" - print "Installing apt packages in packages.txt" - print "--------------------------------------------------" - run("apt-get -qqy update") - run("apt-get -qqy -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" upgrade") - run("apt-get -qqy -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" install {}".format(APT_PACKAGES)) - - # example 3rd paty repo and install certbot - #run("apt-get -qqy install software-properties-common") - #run("add-apt-repository ppa:certbot/certbot -y") - #run("apt-get -qqy update") - #run("apt-get -qqy install python-certbot-apache") - -def run_scripts(): - """ - Run all scripts in the "scripts" directory on the build system - Scripts are run in alpha-numeric order. We recommend naming your scripts - with a name that starts with a two digit number 01-99 to ensure run order. - """ - print "--------------------------------------------------" - print "Running scripts in ./scripts" - print "--------------------------------------------------" - - cwd = os.getcwd() - directory = cwd + "/scripts" - - for f in os.listdir(directory): - - lfile = cwd + "/scripts/" + f - rfile = "/tmp/" + f - print("Processing script in %s" % lfile) - put(lfile,rfile) - run("chmod +x %s" % rfile) - run(rfile) - - -@task -def build_image(): - """ - Configure the build droplet, clean up and shut down for snapshotting - """ - #install_pkgs() - install_files() - run_scripts() - clean_up() - run("exit") - print "----------------------------------------------------------------" - print " Build Complete. Shut down your build droplet from the control" - print " panel before creating your snapshot." - print "----------------------------------------------------------------" - - -@task -def build_test(): - """ - Configure the build droplet, but do not clean up or shut down - """ - #install_pkgs() - install_files() - run_scripts() - print "Build complete. This droplet is NOT ready for use. Use build_image instead of build_test for your final build" diff --git a/util/DigitalOceanMarketplace/files/etc/ufw/applications.d/bitwarden b/util/DigitalOceanMarketplace/files/etc/ufw/applications.d/bitwarden new file mode 100644 index 000000000..c31cda1f1 --- /dev/null +++ b/util/DigitalOceanMarketplace/files/etc/ufw/applications.d/bitwarden @@ -0,0 +1,4 @@ +[Bitwarden] +title=Bitwarden server +description=Bitwarden is an open source password management tool that allows you to securely store, share, and sync passwords and other senitive data. +ports=80/tcp|443/tcp diff --git a/util/DigitalOceanMarketplace/files/opt/bitwarden/install-bitwarden.sh b/util/DigitalOceanMarketplace/files/opt/bitwarden/install-bitwarden.sh old mode 100644 new mode 100755 diff --git a/util/DigitalOceanMarketplace/files/var/lib/cloud/scripts/per-instance/001_onboot b/util/DigitalOceanMarketplace/files/var/lib/cloud/scripts/per-instance/001_onboot old mode 100644 new mode 100755 index e72d50a5c..0a5ad9622 --- a/util/DigitalOceanMarketplace/files/var/lib/cloud/scripts/per-instance/001_onboot +++ b/util/DigitalOceanMarketplace/files/var/lib/cloud/scripts/per-instance/001_onboot @@ -11,7 +11,13 @@ docker pull bitwarden/setup -curl -s -o /root/bitwarden.sh \ - https://raw.githubusercontent.com/bitwarden/server/master/scripts/bitwarden.sh +curl -L -s -o /root/bitwarden.sh https://go.btwrdn.co/bw-sh chmod +x /root/bitwarden.sh + +# Remove the ssh force logout command +sed -e '/Match User root/d' \ + -e '/.*ForceCommand.*droplet.*/d' \ + -i /etc/ssh/sshd_config + +systemctl restart ssh diff --git a/util/DigitalOceanMarketplace/marketplace-image.json b/util/DigitalOceanMarketplace/marketplace-image.json new file mode 100644 index 000000000..52d2cbd69 --- /dev/null +++ b/util/DigitalOceanMarketplace/marketplace-image.json @@ -0,0 +1,86 @@ +{ + "variables": { + "do_token": "{{env `DIGITALOCEAN_TOKEN`}}", + "image_name": "bitwarden-20-04-snapshot-{{timestamp}}", + "apt_packages_1": "fail2ban ca-certificates curl gnupg lsb-release", + "apt_packages_2": "docker-ce docker-ce-cli containerd.io", + "application_name": "Bitwarden", + "application_version": "1.47.1" + }, + "sensitive-variables": [ + "do_token" + ], + "builders": [ + { + "type": "digitalocean", + "api_token": "{{user `do_token`}}", + "image": "ubuntu-20-04-x64", + "region": "nyc3", + "size": "s-1vcpu-1gb", + "ssh_username": "root", + "snapshot_name": "{{user `image_name`}}" + } + ], + "provisioners": [ + { + "type": "shell", + "inline": [ + "cloud-init status --wait" + ] + }, + { + "type": "file", + "source": "files/etc/", + "destination": "/etc/" + }, + { + "type": "file", + "source": "files/opt/", + "destination": "/opt/" + }, + { + "type": "file", + "source": "files/var/", + "destination": "/var/" + }, + { + "type": "shell", + "environment_vars": [ + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ], + "inline": [ + "apt -qqy update", + "apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade", + "apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install {{user `apt_packages_1`}}", + "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg", + "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null", + "apt -qqy update", + "apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install {{user `apt_packages_2`}}", + "apt -qqy clean", + "curl -L \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose", + "chmod +x /usr/local/bin/docker-compose" + ] + }, + { + "type": "shell", + "environment_vars": [ + "application_name={{user `application_name`}}", + "application_version={{user `application_version`}}", + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8" + ], + "scripts": [ + "scripts/01-setup-first-run.sh", + "scripts/02-ufw-bitwarden.sh", + "scripts/03-force-ssh-logout.sh", + "scripts/90-cleanup.sh", + "scripts/99-img-check.sh" + ] + } + ] +} diff --git a/util/DigitalOceanMarketplace/packages.txt b/util/DigitalOceanMarketplace/packages.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/util/DigitalOceanMarketplace/scripts/01-install-docker b/util/DigitalOceanMarketplace/scripts/01-install-docker deleted file mode 100644 index e1bb7f689..000000000 --- a/util/DigitalOceanMarketplace/scripts/01-install-docker +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# Scripts in this directory are run during the build process. -# each script will be uploaded to /tmp on your build droplet, -# given execute permissions and run. The cleanup process will -# remove the scripts from your build system after they have run -# if you use the build_image task. -# - -# -# Install Docker CE -# ref: https://docs.docker.com/install/linux/docker-ce/ubuntu/ -# - -apt-get -y update - -apt-get -y install \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg-agent \ - software-properties-common - -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - - -add-apt-repository \ - "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) \ - stable" - -apt-get -y update - -apt-get -y install docker-ce docker-ce-cli containerd.io diff --git a/util/DigitalOceanMarketplace/scripts/03-setup-first-run b/util/DigitalOceanMarketplace/scripts/01-setup-first-run.sh old mode 100644 new mode 100755 similarity index 80% rename from util/DigitalOceanMarketplace/scripts/03-setup-first-run rename to util/DigitalOceanMarketplace/scripts/01-setup-first-run.sh index 26e4dac15..c8f845746 --- a/util/DigitalOceanMarketplace/scripts/03-setup-first-run +++ b/util/DigitalOceanMarketplace/scripts/01-setup-first-run.sh @@ -17,7 +17,7 @@ chmod +x /etc/update-motd.d/99-bitwarden-welcome # # Setup First Run Script -# ref: https://github.com/digitalocean/marketplace-partners/blob/master/marketplace_docs/build-an-image-fabric.md#running-commands-on-first-login +# ref: https://github.com/digitalocean/marketplace-partners#running-commands-on-first-login # chmod +x /opt/bitwarden/install-bitwarden.sh diff --git a/util/DigitalOceanMarketplace/scripts/02-install-docker-compose b/util/DigitalOceanMarketplace/scripts/02-install-docker-compose deleted file mode 100644 index ed80f2bb8..000000000 --- a/util/DigitalOceanMarketplace/scripts/02-install-docker-compose +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# -# Scripts in this directory are run during the build process. -# each script will be uploaded to /tmp on your build droplet, -# given execute permissions and run. The cleanup process will -# remove the scripts from your build system after they have run -# if you use the build_image task. -# - -# -# Install Docker Compose -# ref: https://docs.docker.com/compose/install/ -# - -curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` \ - -o /usr/local/bin/docker-compose - -chmod +x /usr/local/bin/docker-compose diff --git a/util/DigitalOceanMarketplace/scripts/02-ufw-bitwarden.sh b/util/DigitalOceanMarketplace/scripts/02-ufw-bitwarden.sh new file mode 100755 index 000000000..1f3f0bcfa --- /dev/null +++ b/util/DigitalOceanMarketplace/scripts/02-ufw-bitwarden.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +ufw allow ssh +ufw allow 'Bitwarden' + +ufw --force enable diff --git a/util/DigitalOceanMarketplace/scripts/03-force-ssh-logout.sh b/util/DigitalOceanMarketplace/scripts/03-force-ssh-logout.sh new file mode 100755 index 000000000..d0e979c2c --- /dev/null +++ b/util/DigitalOceanMarketplace/scripts/03-force-ssh-logout.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# DigitalOcean Marketplace Image Validation Tool +# © 2021 DigitalOcean LLC. +# This code is licensed under Apache 2.0 license (see LICENSE.md for details) + +cat >> /etc/ssh/sshd_config < /root/.bash_history +unset HISTFILE +find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; +rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-???????? +rm -rf /var/lib/cloud/instances/* +rm -f /root/.ssh/authorized_keys /etc/ssh/*key* +touch /etc/ssh/revoked_keys +chmod 600 /etc/ssh/revoked_keys + +# Securely erase the unused portion of the filesystem +GREEN='\033[0;32m' +NC='\033[0m' +printf "\n${GREEN}Writing zeros to the remaining disk space to securely +erase the unused portion of the file system. +Depending on your disk size this may take several minutes. +The secure erase will complete successfully when you see:${NC} + dd: writing to '/zerofile': No space left on device\n +Beginning secure erase now\n" + +dd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile diff --git a/util/DigitalOceanMarketplace/scripts/99-img-check.sh b/util/DigitalOceanMarketplace/scripts/99-img-check.sh new file mode 100755 index 000000000..32a9e77ea --- /dev/null +++ b/util/DigitalOceanMarketplace/scripts/99-img-check.sh @@ -0,0 +1,617 @@ +#!/bin/bash + +# DigitalOcean Marketplace Image Validation Tool +# © 2021 DigitalOcean LLC. +# This code is licensed under Apache 2.0 license (see LICENSE.md for details) + +VERSION="v. 1.6" +RUNDATE=$( date ) + +# Script should be run with SUDO +if [ "$EUID" -ne 0 ] + then echo "[Error] - This script must be run with sudo or as the root user." + exit 1 +fi + +STATUS=0 +PASS=0 +WARN=0 +FAIL=0 + +# $1 == command to check for +# returns: 0 == true, 1 == false +cmdExists() { + if command -v "$1" > /dev/null 2>&1; then + return 0 + else + return 1 + fi +} + +function getDistro { + if [ -f /etc/os-release ]; then + # freedesktop.org and systemd + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +elif type lsb_release >/dev/null 2>&1; then + # linuxbase.org + OS=$(lsb_release -si) + VER=$(lsb_release -sr) +elif [ -f /etc/lsb-release ]; then + # For some versions of Debian/Ubuntu without lsb_release command + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE +elif [ -f /etc/debian_version ]; then + # Older Debian/Ubuntu/etc. + OS=Debian + VER=$(cat /etc/debian_version) +elif [ -f /etc/SuSe-release ]; then + # Older SuSE/etc. + : +elif [ -f /etc/redhat-release ]; then + # Older Red Hat, CentOS, etc. + VER=$( cat /etc/redhat-release | cut -d" " -f3 | cut -d "." -f1) + d=$( cat /etc/redhat-release | cut -d" " -f1 | cut -d "." -f1) + if [[ $d == "CentOS" ]]; then + OS="CentOS Linux" + fi +else + # Fall back to uname, e.g. "Linux ", also works for BSD, etc. + OS=$(uname -s) + VER=$(uname -r) +fi +} +function loadPasswords { +SHADOW=$(cat /etc/shadow) +} + +function checkAgent { + # Check for the presence of the do-agent in the filesystem + if [ -d /var/opt/digitalocean/do-agent ];then + echo -en "\e[41m[FAIL]\e[0m DigitalOcean Monitoring Agent detected.\n" + ((FAIL++)) + STATUS=2 + if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then + echo "The agent can be removed with 'sudo yum remove do-agent' " + elif [[ $OS == "Ubuntu" ]]; then + echo "The agent can be removed with 'sudo apt-get purge do-agent' " + fi + else + echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n" + ((PASS++)) + fi +} + +function checkLogs { + cp_ignore="/var/log/cpanel-install.log" + echo -en "\nChecking for log files in /var/log\n\n" + # Check if there are log archives or log files that have not been recently cleared. + for f in /var/log/*-????????; do + [[ -e $f ]] || break + if [ $f != $cp_ignore ]; then + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + done + for f in /var/log/*.[0-9];do + [[ -e $f ]] || break + echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + done + for f in /var/log/*.log; do + [[ -e $f ]] || break + if [[ "${f}" = '/var/log/lfd.log' && "$( cat "${f}" | egrep -v '/var/log/messages has been reset| Watching /var/log/messages' | wc -c)" -gt 50 ]]; then + if [ $f != $cp_ignore ]; then + echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + elif [[ "${f}" != '/var/log/lfd.log' && "$( cat "${f}" | wc -c)" -gt 50 ]]; then + if [ $f != $cp_ignore ]; then + echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + fi + done +} +function checkTMP { + # Check the /tmp directory to ensure it is empty. Warn on any files found. + return 1 +} +function checkRoot { + user="root" + uhome="/root" + for usr in $SHADOW + do + IFS=':' read -r -a u <<< "$usr" + if [[ "${u[0]}" == "${user}" ]]; then + if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then + echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n" + ((FAIL++)) + STATUS=2 + fi + fi + done + if [ -d ${uhome}/ ]; then + if [ -d ${uhome}/.ssh/ ]; then + if ls ${uhome}/.ssh/*> /dev/null 2>&1; then + for key in ${uhome}/.ssh/* + do + if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then + + if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n" + akey=$(cat ${key}) + echo "File Contents:" + echo $akey + echo "--------------" + ((FAIL++)) + STATUS=2 + fi + elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then + if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then + echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n" + akey=$(cat ${key}) + echo "File Contents:" + echo $akey + echo "--------------" + ((FAIL++)) + STATUS=2 + else + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + else + if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + fi + done + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n" + fi + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n" + fi + if [ -f /root/.bash_history ];then + + BH_S=$( cat /root/.bash_history | wc -c) + + if [[ $BH_S -lt 200 ]]; then + echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n" + ((FAIL++)) + STATUS=2 + fi + + return 1; + else + echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n" + ((PASS++)) + fi + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n" + fi + echo -en "\n\n" + return 1 +} + +function checkUsers { + # Check each user-created account + for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd;) + do + # Skip some other non-user system accounts + if [[ $user == "centos" ]]; then + : + elif [[ $user == "nfsnobody" ]]; then + : + else + echo -en "\nChecking user: ${user}...\n" + for usr in $SHADOW + do + IFS=':' read -r -a u <<< "$usr" + if [[ "${u[0]}" == "${user}" ]]; then + if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then + echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n" + ((FAIL++)) + STATUS=2 + fi + fi + done + #echo "User Found: ${user}" + uhome="/home/${user}" + if [ -d "${uhome}/" ]; then + if [ -d "${uhome}/.ssh/" ]; then + if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then + for key in ${uhome}/.ssh/* + do + if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then + if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then + echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n" + akey=$(cat ${key}) + echo "File Contents:" + echo $akey + echo "--------------" + ((FAIL++)) + STATUS=2 + fi + elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then + if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then + echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n" + akey=$(cat ${key}) + echo "File Contents:" + echo $akey + echo "--------------" + ((FAIL++)) + STATUS=2 + else + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then + + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + + else + if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then + echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + fi + + + done + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n" + fi + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n" + fi + else + echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n" + fi + + # Check for an uncleared .bash_history for this user + if [ -f "${uhome}/.bash_history" ]; then + BH_S=$( cat "${uhome}/.bash_history" | wc -c ) + + if [[ $BH_S -lt 200 ]]; then + echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n" + ((PASS++)) + else + echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n" + ((FAIL++)) + STATUS=2 + + fi + echo -en "\n\n" + fi + fi + done +} +function checkFirewall { + + if [[ $OS == "Ubuntu" ]]; then + fw="ufw" + ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //") + if [[ $ufwa == "active" ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then + if [ -f /usr/lib/systemd/system/csf.service ]; then + fw="csf" + if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then + + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + elif cmdExists "firewall-cmd"; then + if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + else + fw="firewalld" + if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + fi + elif [[ "$OS" =~ Debian.* ]]; then + # user could be using a number of different services for managing their firewall + # we will check some of the most common + if cmdExists 'ufw'; then + fw="ufw" + ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //") + if [[ $ufwa == "active" ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + elif cmdExists "firewall-cmd"; then + fw="firewalld" + if [[ $(systemctl is-active --quiet $fw) ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + else + # user could be using vanilla iptables, check if kernel module is loaded + fw="iptables" + if [[ $(lsmod | grep -q '^ip_tables' 2>/dev/null) ]]; then + FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n" + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + ((WARN++)) + fi + fi + fi + +} +function checkUpdates { + if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then + # Ensure /tmp exists and has the proper permissions before + # checking for security updates + # https://github.com/digitalocean/marketplace-partners/issues/94 + if [[ ! -d /tmp ]]; then + mkdir /tmp + fi + chmod 1777 /tmp + + echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n" + apt-get -y update > /dev/null + + uc=$(apt-get --just-print upgrade | grep -i "security" | wc -l) + if [[ $uc -gt 0 ]]; then + update_count=$(( ${uc} / 2 )) + else + update_count=0 + fi + + if [[ $update_count -gt 0 ]]; then + echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n" + echo -en + echo -en "Here is a list of the security updates that are not installed:\n" + sleep 2 + apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++' + echo -en + ((FAIL++)) + STATUS=2 + else + echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n" + fi + elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]]; then + echo -en "\nChecking for available security updates, this may take a minute...\n\n" + + update_count=$(yum check-update --security --quiet | wc -l) + if [[ $update_count -gt 0 ]]; then + echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n" + ((FAIL++)) + STATUS=2 + else + echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n" + ((PASS++)) + fi + else + echo "Error encountered" + exit 1 + fi + + return 1; +} +function checkCloudInit { + + if hash cloud-init 2>/dev/null; then + CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n" + ((PASS++)) + else + CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n" + ((FAIL++)) + STATUS=2 + fi + return 1 +} + +function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; } + + +clear +echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}" +echo "Executed on: ${RUNDATE}" +echo "Checking local system for Marketplace compatibility..." + +getDistro + +echo -en "\n\e[1mDistribution:\e[0m ${OS}\n" +echo -en "\e[1mVersion:\e[0m ${VER}\n\n" + +ost=0 +osv=0 + +if [[ $OS == "Ubuntu" ]]; then + ost=1 + if [[ $VER == "20.04" ]]; then + osv=1 + elif [[ $VER == "18.04" ]]; then + osv=1 + elif [[ $VER == "16.04" ]]; then + osv=1 + else + osv=0 + fi + +elif [[ "$OS" =~ Debian.* ]]; then + ost=1 + case "$VER" in + 9) + osv=1 + ;; + 10) + osv=1 + ;; + *) + osv=2 + ;; + esac + +elif [[ $OS == "CentOS Linux" ]]; then + ost=1 + if [[ $VER == "8" ]]; then + osv=1 + elif [[ $VER == "7" ]]; then + osv=1 + elif [[ $VER == "6" ]]; then + osv=1 + else + osv=2 + fi +elif [[ $OS == "CentOS Stream" ]]; then + ost=1 + if [[ $VER == "8" ]]; then + osv=1 + else + osv=2 + fi +elif [[ $OS == "Rocky Linux" ]]; then + ost=1 + if [[ $VER =~ "8." ]]; then + osv=1 + else + osv=2 + fi +else + ost=0 +fi + +if [[ $ost == 1 ]]; then + echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n" + ((PASS++)) +else + echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n" + ((FAIL++)) + STATUS=2 +fi + +if [[ $osv == 1 ]]; then + echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n" + ((PASS++)) +elif [[ $ost == 1 ]]; then + echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n" + ((FAIL++)) + STATUS=2 +else + echo "Exiting..." + exit 1 +fi + +checkCloudInit + +echo -en "${CI}" + +checkFirewall + +echo -en "${FW_VER}" + +checkUpdates + +loadPasswords + +checkLogs + +echo -en "\n\nChecking all user-created accounts...\n" +checkUsers + +echo -en "\n\nChecking the root account...\n" +checkRoot + +checkAgent + + +# Summary +echo -en "\n\n---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n" +elif [[ $STATUS == 1 ]]; then + echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n" +else + echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n" +fi +echo "---------------------------------------------------------------------------------------------------" +echo -en "\e[1m${PASS} Tests PASSED\e[0m\n" +echo -en "\e[1m${WARN} WARNINGS\e[0m\n" +echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n" +echo -en "---------------------------------------------------------------------------------------------------\n" + +if [[ $STATUS == 0 ]]; then + echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n" + exit 0 +elif [[ $STATUS == 1 ]]; then + echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n" + exit 0 +else + echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n" + exit 1 +fi