diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e4ac33938e4..2ed0a039b83 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -34,6 +34,23 @@ jobs: config: '.github/linters/.markdown-lint.yml' args: './README.md' + packer: + name: packer + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup packer + uses: hashicorp/setup-packer@main + with: + version: latest + - name: Run packer init + run: "make image/init/digitalocean" + - name: Run `packer validate` + env: + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + run: "make image/validate/digitalocean" + shellcheck: name: shellcheck runs-on: ubuntu-20.04 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edc508e810c..56418379360 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,6 +72,40 @@ jobs: name: build path: build + build-digitalocean: + name: build-digitalocean + runs-on: ubuntu-20.04 + needs: release + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup packer + uses: hashicorp/setup-packer@main + with: + version: latest + + - name: Run packer init + run: "make image/init/digitalocean" + + - name: Run `packer validate` + env: + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + PKR_VAR_dokku_version: ${{ needs.release.outputs.version }} + run: "make image/validate/digitalocean" + + - name: Bump Digitalocean Image + env: + DIGITALOCEAN_TOKEN: ${{ secrets.DIGITALOCEAN_TOKEN }} + PKR_VAR_dokku_version: ${{ needs.release.outputs.version }} + run: "make image/build/digitalocean" + + - name: upload packages + uses: actions/upload-artifact@v3 + with: + name: digitalocean-manifest.json + path: digitalocean-manifest.json + bump-azure: name: bump-azure runs-on: ubuntu-20.04 diff --git a/contrib/images/digitalocean/files/etc/nginx/sites-available/digitalocean b/contrib/images/digitalocean/files/etc/nginx/sites-available/digitalocean new file mode 100644 index 00000000000..9e9b34ce931 --- /dev/null +++ b/contrib/images/digitalocean/files/etc/nginx/sites-available/digitalocean @@ -0,0 +1,95 @@ +# You may add here your +# server { +# ... +# } +# statements for each of your virtual hosts to this file + +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +server { + listen 80 default_server; + listen [::]:80 default_server ipv6only=on; + + root /var/www/html; + index index.php index.html index.htm; + + # Make site accessible from http://localhost/ + server_name localhost; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + # Uncomment to enable naxsi on this location + # include /etc/nginx/naxsi.rules + } + + error_page 404 /404.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php7.4-fpm.sock; + } + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# another virtual host using mix of IP-, name-, and port-based configuration +# +#server { +# listen 8000; +# listen somename:8080; +# server_name somename alias another.alias; +# root html; +# index index.html index.htm; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} + + +# HTTPS server +# +#server { +# listen 443; +# server_name localhost; +# +# root html; +# index index.html index.htm; +# +# ssl on; +# ssl_certificate cert.pem; +# ssl_certificate_key cert.key; +# +# ssl_session_timeout 5m; +# +# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; +# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; +# ssl_prefer_server_ciphers on; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/contrib/images/digitalocean/files/etc/update-motd.d/99-one-click b/contrib/images/digitalocean/files/etc/update-motd.d/99-one-click new file mode 100755 index 00000000000..c4c8f37ddd6 --- /dev/null +++ b/contrib/images/digitalocean/files/etc/update-motd.d/99-one-click @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Configured as part of the DigitalOcean 1-Click Image build process + +myip=$(hostname -I | awk '{print$1}') +cat < >(tee /var/log/one_click_setup.log) 2>&1 + +ufw --force enable +printf "dokku dokku/hostname string %s" hostname | debconf-set-selections + +export DEBIAN_FRONTEND=noninteractive +export LANG=C +export LC_ALL=C + +# Install Dokku.... +for count in {1..30}; do + dpkg -i /var/lib/digitalocean/debs/*deb || /bin/true + apt-get -f install || /bin/true + + version=$(dpkg-query --showformat='${Version}' -W dokku) + if [[ -n "$version" ]]; then + ran=0 + exit 0 + else + count=$((count + 1)) + fi + sleep 5 +done diff --git a/contrib/images/digitalocean/files/var/lib/cloud/scripts/per-once/002_enable_ssh b/contrib/images/digitalocean/files/var/lib/cloud/scripts/per-once/002_enable_ssh new file mode 100755 index 00000000000..48bd764a51c --- /dev/null +++ b/contrib/images/digitalocean/files/var/lib/cloud/scripts/per-once/002_enable_ssh @@ -0,0 +1,9 @@ +#!/bin/bash +exec > >(tee /var/log/one_click_setup.log) 2>&1 + +# Remove the force command +sed -e '/Match user root/d' \ + -e '/.*ForceCommand.*droplet.*/d' \ + -i /etc/ssh/sshd_config + +systemctl restart ssh diff --git a/contrib/images/digitalocean/files/var/www/html/index.html b/contrib/images/digitalocean/files/var/www/html/index.html new file mode 100644 index 00000000000..989a2af6a96 --- /dev/null +++ b/contrib/images/digitalocean/files/var/www/html/index.html @@ -0,0 +1,169 @@ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Please log into your Droplet with SSH to configure the Dokku installation.

+

See the Dokku 1-Click Quickstart guide for detailed assistance.

+ Quickstart Guide +
+ + + diff --git a/contrib/images/digitalocean/in_parts/011-docker b/contrib/images/digitalocean/in_parts/011-docker new file mode 100755 index 00000000000..6bb7c6b6682 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/011-docker @@ -0,0 +1,47 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +export DEBIAN_FRONTEND=noninteractive + +pkgs=(apt-transport-https + ca-certificates + curl + jq + linux-image-extra-virtual + software-properties-common +) + +echo '--> Updating apt repositories' +apt-get -qqy update + +echo '--> Updating all packages' +apt-get -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade + +echo "--> Installing basic dependencies: ${pkgs[*]}" +apt-get -qqy install "${pkgs[@]}" + +echo '--> Setting up docker apt repository' +curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + +cat >/etc/apt/sources.list.d/docker.list < Installing docker-ce' +apt-get -y update +apt-get -y install docker-ce docker-compose-plugin + +echo '--> Enabling docker-ce' +systemctl enable docker +systemctl start docker + +echo '--> Enabling docker live-restore' +if [[ ! -f /etc/docker/daemon.json ]]; then + echo "{}" >/etc/docker/daemon.json +fi + +config="$(jq '. + {"live-restore": true}' /etc/docker/daemon.json)" +echo "$config" >/etc/docker/daemon.json +systemctl reload docker +docker info | grep -i live diff --git a/contrib/images/digitalocean/in_parts/011-ssh-message b/contrib/images/digitalocean/in_parts/011-ssh-message new file mode 100755 index 00000000000..83db94286d7 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/011-ssh-message @@ -0,0 +1,10 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +echo '--> Prevent login until after setup is complete' +# Be a bit of a dork about login in +cat >>/etc/ssh/sshd_config < Install core dependencies for dokku' +apt-get -y install "${pkgs[@]}" + +echo '--> Setting up dokku apt repository' +curl -fsSL https://packagecloud.io/dokku/dokku/gpgkey -o /etc/apt/keyrings/dokku.asc + +cat >/etc/apt/sources.list.d/dokku.list < Install dependencies for dokku from new repository' +apt-get -y clean +apt-get -y update +apt-get -y install "${dokku_pkgs[@]}" +rm -f /etc/nginx/sites-enabled/default + +echo '--> Pre-cache dokku bits that are must run on boot' +apt-get -y clean +apt-get -y install --download-only "dokku=${DOKKU_VERSION}" dokku-event-listener herokuish + +mkdir -p /var/lib/digitalocean/debs +cp -au /var/cache/apt/archives/*deb /var/lib/digitalocean/debs diff --git a/contrib/images/digitalocean/in_parts/012-grub-opts b/contrib/images/digitalocean/in_parts/012-grub-opts new file mode 100755 index 00000000000..fbb043cb596 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/012-grub-opts @@ -0,0 +1,9 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +echo '--> Update grub boot loader' +sed -e 's|GRUB_CMDLINE_LINUX="|GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1|g' \ + -i /etc/default/grub + +update-grub diff --git a/contrib/images/digitalocean/in_parts/014-docker-dns b/contrib/images/digitalocean/in_parts/014-docker-dns new file mode 100755 index 00000000000..e3b509edc49 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/014-docker-dns @@ -0,0 +1,7 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +echo '--> Enable Google DNS for Docker' +sed -e 's|#DOCKER_OPTS|DOCKER_OPTS|g' \ + -i /etc/default/docker diff --git a/contrib/images/digitalocean/in_parts/014-ufw-rules b/contrib/images/digitalocean/in_parts/014-ufw-rules new file mode 100755 index 00000000000..957728b28eb --- /dev/null +++ b/contrib/images/digitalocean/in_parts/014-ufw-rules @@ -0,0 +1,12 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +echo '--> Enable and configure UFW' +sed -e 's|DEFAULT_FORWARD_POLICY=.*|DEFAULT_FORWARD_POLICY="ACCEPT"|g' \ + -i /etc/default/ufw + +ufw limit ssh +ufw allow 'Nginx Full' + +ufw --force enable diff --git a/contrib/images/digitalocean/in_parts/099-application_tag b/contrib/images/digitalocean/in_parts/099-application_tag new file mode 100755 index 00000000000..97a1f1617d0 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/099-application_tag @@ -0,0 +1,21 @@ +#!/bin/bash +set -eo pipefail +set -o errexit + +echo '--> Write the application info' +application_version="$DOKKU_VERSION" +build_date="$(date -I)" +distro_arch="$(uname -m)" +distro_codename="$(lsb_release -sc)" +distro_release="$(lsb_release -sr)" +distro="$(lsb_release -si)" + +cat >/var/lib/digitalocean/application.info < Cleaning up tmp dir' +# 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 +rm -rf /tmp/* /var/tmp/* + +echo '--> Cleaning up apt' +export DEBIAN_FRONTEND=noninteractive +apt-get -y update +apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes +apt-get -y autoremove +apt-get -y autoclean +apt-get -y purge droplet-agent + +echo '--> Clearing history' +history -c +cat /dev/null >/root/.bash_history +unset HISTFILE + +echo '--> Clearing log files' +find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; +rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-???????? +cat /dev/null >/var/log/lastlog +cat /dev/null >/var/log/wtmp +rm -rf /var/lib/cloud/instances/* +rm -rf /tmp/* /var/tmp/* + +echo '--> Removing keys' +rm -f /root/.ssh/authorized_keys /etc/ssh/*key* +touch /etc/ssh/revoked_keys +chmod 600 /etc/ssh/revoked_keys + +echo '--> DO Directory contents' +ls -lah /opt/digitalocean || true + +echo '--> Securely erase the unused portion of the filesystem' +printf "\n\033[0;32mWriting 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:\033[0m + 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/contrib/images/digitalocean/in_parts/100-image-check b/contrib/images/digitalocean/in_parts/100-image-check new file mode 100755 index 00000000000..a14bb87c121 --- /dev/null +++ b/contrib/images/digitalocean/in_parts/100-image-check @@ -0,0 +1,636 @@ +#!/bin/bash + +# DigitalOcean Marketplace Image Validation Tool +# © 2021-2022 DigitalOcean LLC. +# This code is licensed under Apache 2.0 license (see LICENSE.md for details) + +VERSION="v. 1.8.1" +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 + # shellcheck disable=SC1091 + . /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 + # shellcheck disable=SC1091 + . /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=$(cut -d" " -f3 ", 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 directory in the filesystem + if [ -d /opt/digitalocean ]; then + echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n" + ((FAIL++)) + STATUS=2 + if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; then + echo "To uninstall the agent: 'sudo yum remove droplet-agent'" + echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'" + elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then + echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-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; Contents:\n" + cat "${f}" + ((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; Contents:\n" + cat "${f}" + ((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' && "$(grep -E -v '/var/log/messages has been reset| Watching /var/log/messages' "${f}" | wc -c)" -gt 50 ]]; then + if [ "${f}" != "${cp_ignore}" ]; then + echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n" + cat "${f}" + ((WARN++)) + if [[ $STATUS != 2 ]]; then + STATUS=1 + fi + fi + elif [[ "${f}" != '/var/log/lfd.log' && "$(wc -c <"${f}")" -gt 50 ]]; then + if [ "${f}" != "${cp_ignore}" ]; then + echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n" + cat "${f}" + ((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. + if [[ -n "$(ls -A /tmp)" ]]; then + echo -en "\e[93m[WARN]\e[0m /tmp directory is not empty; Contents\n" + ls -A /tmp + ((WARN++)) + return 1 + fi + + echo -en "\e[32m[PASS]\e[0m /tmp directory is empty\n" + return 0 +} +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 [ "$(wc -c <"${key}")" -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 [ "$(wc -c <"${key}")" -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 [ "$(wc -c <"${key}")" -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=$(wc -c = 1000 && $1 != "nobody" {print $1}' /dev/null 2>&1; then + for key in "${uhome}"/.ssh/*; do + if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then + if [ "$(wc -c <"${key}")" -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 [ "$(wc -c <"${key}")" -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" + # shellcheck disable=SC2030 + ((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 [ "$(wc -c <"${key}")" -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=$(wc -c <"${uhome}/.bash_history") + + 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" + # shellcheck disable=SC2031 + ((PASS++)) + else + FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n" + # shellcheck disable=SC2031 + ((WARN++)) + fi + elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; 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" -c) + 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 + # shellcheck disable=SC2031 + ((FAIL++)) + STATUS=2 + else + echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n" + ((PASS++)) + fi + elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; 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 +} + +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 == "22.10" ]] || [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then + osv=1 + fi + +elif [[ "$OS" =~ Debian.* ]]; then + ost=1 + case "$VER" in + 9) + osv=1 + ;; + 10) + osv=1 + ;; + 11) + 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 +elif [[ $OS == "AlmaLinux" ]]; then + ost=1 + if [[ "$VERSION" =~ 8.* ]] || [[ "$VERSION" =~ 9.* ]]; 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 + +echo -en "\n\nChecking the /tmp directory...\n" +checkTMP + +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 diff --git a/contrib/images/digitalocean/packer.pkr.hcl b/contrib/images/digitalocean/packer.pkr.hcl new file mode 100644 index 00000000000..c0219a05a15 --- /dev/null +++ b/contrib/images/digitalocean/packer.pkr.hcl @@ -0,0 +1,70 @@ +packer { + required_plugins { + digitalocean = { + version = ">= 1.0.4" + source = "github.com/digitalocean/digitalocean" + } + } +} + +variable "dokku_version" { + type = string +} + +source "digitalocean" "ubuntu" { + image = "ubuntu-22-04-x64" + region = "nyc1" + size = "s-1vcpu-512mb-10gb" + ssh_username = "root" + snapshot_name = "dokku-${var.dokku_version}-snapshot-{{timestamp}}" +} + +build { + name = "dokku" + sources = [ + "source.digitalocean.ubuntu" + ] + + provisioner "shell" { + inline = [ + "echo '--> Waiting until cloud-init is complete'", + "/usr/bin/cloud-init status --wait", + ] + } + + provisioner "file" { + source = "${path.root}/files/etc/" + destination = "/etc/" + } + + provisioner "file" { + source = "${path.root}/files/var/" + destination = "/var/" + } + + provisioner "shell" { + environment_vars = [ + "DOKKU_VERSION=${var.dokku_version}", + "DEBIAN_FRONTEND=noninteractive", + "LC_ALL=C", + "LANG=en_US.UTF-8", + "LC_CTYPE=en_US.UTF-8", + ] + + scripts = [ + "${path.root}/in_parts/011-docker", + "${path.root}/in_parts/011-ssh-message", + "${path.root}/in_parts/012-dokku-packages", + "${path.root}/in_parts/012-grub-opts", + "${path.root}/in_parts/014-docker-dns", + "${path.root}/in_parts/014-ufw-rules", + "${path.root}/in_parts/099-application_tag", + "${path.root}/in_parts/099-cleanup", + "${path.root}/in_parts/100-image-check", + ] + } + + post-processor "manifest" { + output = "digitalocean-manifest.json" + } +} diff --git a/contrib/packer.json b/contrib/packer.json deleted file mode 100644 index 96a60e5b2f4..00000000000 --- a/contrib/packer.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "builders": [ - { - "type": "digitalocean", - "region": "nyc3", - "image": "ubuntu-14-04-x64" - } - ], - "provisioners": [ - { - "type": "file", - "source": "deb.mk", - "destination": "/tmp/Makefile" - }, - { - "type": "shell", - "inline": [ - "echo '--> Waiting 30 seconds for core services to be available'", - "sleep 30", - - "echo '--> Updating apt repositories'", - "sudo apt-get update -qq >/dev/null", - - "echo '--> Installing make requirement'", - "sudo apt-get -qq -y --no-install-recommends install build-essential", - - "cd /tmp && make install-from-deb", - "rm /root/.ssh/authorized_keys" - ] - } - ] -} diff --git a/package.mk b/package.mk index dbfa1eaac58..8a92a3393c4 100644 --- a/package.mk +++ b/package.mk @@ -1,3 +1,7 @@ +ifndef PKR_VAR_dokku_version + PKR_VAR_dokku_version = $(shell grep Version debian/control | cut -d' ' -f2) +endif + /tmp/build-dokku/var/lib/dokku/GIT_REV: mkdir -p /tmp/build-dokku mkdir -p /tmp/build-dokku/usr/share/bash-completion/completions @@ -31,3 +35,15 @@ ifdef DOKKU_GIT_REV else git rev-parse HEAD > /tmp/build-dokku/var/lib/dokku/GIT_REV endif + +.PHONY: image/build/digitalocean +image/build/digitalocean: + packer build -var 'dokku_version=${PKR_VAR_dokku_version}' contrib/images/digitalocean/packer.pkr.hcl + +.PHONY: image/init/digitalocean +image/init/digitalocean: + packer init contrib/images/digitalocean/packer.pkr.hcl + +.PHONY: image/validate/digitalocean +image/validate/digitalocean: + packer validate -var 'dokku_version=${PKR_VAR_dokku_version}' contrib/images/digitalocean/packer.pkr.hcl