diff --git a/README.md b/README.md index aed38f8..d2b6ccc 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,234 @@ # imagehost-setup -A single-script installer that turns a fresh AlmaLinux 10 VPS into a -lightweight, secure image host — SFTP upload, Nginx serving, optional -Let's Encrypt SSL, and fail2ban out of the box. +A single-script installer that turns a fresh AlmaLinux 10 VPS into a lightweight, secure image host — SFTP upload, Nginx file serving, optional Let's Encrypt SSL, and brute-force protection out of the box. ## Requirements -- A fresh AlmaLinux 10 VPS +- A fresh AlmaLinux 10 VPS (tested on Linode/Akamai) +- A domain name pointed at your server's IP address *(optional but recommended for SSL)* - Root or sudo access -## Usage +## Quick Start *(for experienced users)* -Download and run the script: +```bash +curl -O https://git.castlehollow.com/rodger/imagehost-setup/raw/branch/main/imagehost-setup.sh +sudo bash imagehost-setup.sh +``` - curl -O https://git.castlehollow.com/rodger/imagehost-setup/raw/branch/main/imagehost-setup.sh - sudo bash imagehost-setup.sh - -The script will walk you through the rest interactively. +The script will walk you through the rest interactively. When it finishes, it prints your SFTP credentials and image URL — save them somewhere safe. ## What it sets up -- **Nginx** — serves image files only (JPG, PNG, GIF, WebP, AVIF, SVG, BMP, TIFF) -- **SFTP chroot** — a locked-down upload user with no shell access -- **firewalld** — opens only SSH, HTTP, and HTTPS -- **fail2ban** — brute-force protection on SSH and Nginx -- **Let's Encrypt SSL** — optional, with HTTP fallback +- **Nginx** — serves image files only (JPG, PNG, GIF, WebP, AVIF, SVG, BMP, TIFF); everything else returns a 404 +- **SFTP chroot** — a locked-down upload user that can only access the images folder; no shell access +- **Maintenance user** — a normal SSH login with sudo access for administration +- **firewalld** — opens only SSH, HTTP, and HTTPS; everything else is blocked +- **fail2ban** — automatically bans IPs that repeatedly fail to log in +- **Let's Encrypt SSL** — optional; falls back to plain HTTP if no domain is provided + +--- + +## Guide for Windows Users New to Linux + +Never used a terminal before? No problem. This section walks you through everything from zero. + +### Before You Start + +You'll need to have already done two things that aren't covered here: + +1. **Rented a VPS** (Virtual Private Server) — a small cloud computer you can run 24/7. Linode/Akamai is a good choice. Their smallest plan (Nanode, $5/month) is plenty for this. +2. **Set up a domain name** *(optional)* — if you want a proper web address like `images.yourdomain.com` instead of a raw IP address, you'll need a domain and need to point it at your server's IP address. Your VPS provider and domain registrar will have guides for this. + +If you need help with either of those, check the video walkthrough [link here]. + +Once your server is running and you have its **IP address** (looks something like `172.237.151.226`) and **root password** from your provider, come back here. + +--- + +### Step 1 — Get a Terminal on Windows + +A terminal (also called a command prompt or console) is a text-based window you type commands into. On modern Windows you have a few options: + +**Option A — Windows Terminal + SSH (Windows 10/11, recommended)** + +Windows 10 and 11 come with SSH built in. Press the **Windows key**, type `Terminal`, and open **Windows Terminal** or **PowerShell**. Either one works fine. + +**Option B — PuTTY (older Windows, or if the above doesn't work)** + +Download PuTTY from [https://www.putty.org](https://www.putty.org) — it's free and has been the standard Windows SSH client for decades. Install it and open it. + +--- + +### Step 2 — Log Into Your Server + +Your server is running Linux and is waiting for you to connect to it. The way you connect is called **SSH** (Secure Shell) — it's an encrypted connection that lets you type commands on the remote server as if you were sitting in front of it. + +**Using Windows Terminal or PowerShell:** + +Type the following, replacing `YOUR.SERVER.IP` with your actual IP address: + +``` +ssh root@YOUR.SERVER.IP +``` + +Press Enter. You'll see a message like: + +``` +The authenticity of host '172.237.151.226' can't be established. +Are you sure you want to continue connecting (yes/no)? +``` + +Type `yes` and press Enter. This is normal — it's just your computer remembering the server for next time. + +Then it will ask for a password. Type your root password (the one your VPS provider gave you) and press Enter. **You won't see anything as you type — that's normal**, Linux hides passwords for security. + +You should end up at a prompt that looks something like: + +``` +[root@localhost ~]# +``` + +You're in. You're now typing commands directly on your server. + +**Using PuTTY:** + +Open PuTTY. In the **Host Name** box, type your server's IP address. Make sure **Port** is `22` and **SSH** is selected. Click **Open**. When the security warning appears, click **Accept**. Log in as `root` with your server password. + +--- + +### Step 3 — Download and Run the Setup Script + +Now you'll download and run the installer. Copy and paste these two commands, pressing Enter after each one: + +```bash +curl -O https://git.castlehollow.com/rodger/imagehost-setup/raw/branch/main/imagehost-setup.sh +``` + +```bash +bash imagehost-setup.sh +``` + +> **Tip:** To paste into Windows Terminal, right-click or press **Ctrl+Shift+V**. In PuTTY, just right-click. + +The script will now ask you a series of questions. Here's what each one means: + +--- + +### Step 4 — Answering the Setup Questions + +**Domain name** +If you have a domain name pointed at this server (like `images.yourdomain.com`), type it here and press Enter. If not, just press Enter to skip — your images will be accessible by IP address instead. + +**Email address** +Only asked if you entered a domain. This is used by Let's Encrypt to send you certificate expiry notices. Enter any email address you check. + +**SFTP username** +This is the username you'll use to upload images. Press Enter to accept the default (`imageuser`), or type your own. + +**SFTP password** +This is the password for uploading images. You can type your own password or just press Enter to have a strong one generated for you automatically. Either way, it will be displayed at the end — make sure you save it. + +**Maintenance username** +This is a separate login for managing the server itself (not for uploading images). Press Enter to accept the default (`siteadmin`), or type your own. + +**Maintenance password** +Same as above — type one or press Enter to auto-generate. + +**Maximum image file size** +The largest file size (in megabytes) that can be uploaded. Press Enter to accept the default of 20 MB, or type a number. + +**Proceed with installation?** +Review the summary and type `y` then Enter to start the installation. The script will now run for a few minutes — you'll see progress messages scrolling by. This is normal. + +--- + +### Step 5 — Save Your Credentials + +When the script finishes, it will print a summary that looks something like this: + +``` + Image URL format: + https://images.yourdomain.com/ + + SFTP connection details: + Host : images.yourdomain.com + Port : 22 + Username : imageuser + Password : Xk92mPqL... ← Save this now! + Upload to: /images/ + + Maintenance (SSH) login: + Host : images.yourdomain.com + Port : 22 + Username : siteadmin + Password : Rt47vNwM... ← Save this now! + Sudo : sudo -i to become root +``` + +**Copy this entire block and save it somewhere safe** — a password manager, a secure note, anywhere you won't lose it. The passwords cannot be recovered after this point (though they can be reset if needed). + +--- + +### Step 6 — Upload Your First Image + +You'll need an SFTP client — a program that lets you transfer files to your server. These are all free: + +- **FileZilla** — [https://filezilla-project.org](https://filezilla-project.org) (Windows, Mac, Linux) +- **WinSCP** — [https://winscp.net](https://winscp.net) (Windows only, very beginner-friendly) +- **Cyberduck** — [https://cyberduck.io](https://cyberduck.io) (Windows and Mac) + +**Connecting with FileZilla (as an example):** + +1. Open FileZilla +2. At the top, fill in: + - **Host:** your domain or IP address + - **Username:** your SFTP username (e.g. `imageuser`) + - **Password:** your SFTP password + - **Port:** `22` +3. Click **Quickconnect** +4. On the right side you'll see a folder called `images` — that's where your files go +5. Drag an image from your computer into that folder + +**Accessing your image:** + +Once uploaded, your image is immediately available at: + +``` +https://images.yourdomain.com/your-filename.jpg +``` + +Or if you're using an IP address: + +``` +http://172.237.151.226/your-filename.jpg +``` + +That's the URL you'd paste into your inventory management system listing. + +--- + +### Troubleshooting + +**"Connection refused" when trying to SSH** +Your server may still be booting, or your IP address is wrong. Wait a minute and try again. Double-check the IP in your VPS provider's dashboard. + +**Images returning a 404 error** +Make sure you uploaded the file into the `/images/` folder, not the root of the SFTP connection. In FileZilla, you should see an `images` folder when you first connect — put your files inside that. + +**Forgot the SFTP or maintenance password** +Log into your server via SSH as your maintenance user, then run: +```bash +sudo passwd imageuser +``` +Replace `imageuser` with whichever account needs a password reset. You'll be prompted to set a new one. + +**SSL certificate didn't install / getting a security warning** +This usually means your domain's DNS record wasn't pointing at your server's IP yet when the script ran. Once DNS is set up correctly, log in via SSH and run: +```bash +sudo certbot --nginx -d images.yourdomain.com +``` + +--- ## License diff --git a/imagehost-setup.sh~ b/imagehost-setup.sh~ deleted file mode 100644 index 0da8129..0000000 --- a/imagehost-setup.sh~ +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# imagehost-setup.sh -# Turns a fresh AlmaLinux 10 VPS into a lightweight, secure image host. -# - Nginx (image-type-restricted static file serving) -# - Chrooted SFTP-only upload user -# - firewalld rules -# - fail2ban -# - Optional Let's Encrypt SSL (falls back to plain HTTP) -# -# Usage: sudo bash imagehost-setup.sh -# ============================================================================= - -set -euo pipefail - -# ── Colour helpers ──────────────────────────────────────────────────────────── -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' -CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' - -info() { echo -e "${CYAN}[INFO]${RESET} $*"; } -success() { echo -e "${GREEN}[OK]${RESET} $*"; } -warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } -die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } -banner() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════════${RESET}"; \ - echo -e "${BOLD}${CYAN} $*${RESET}"; \ - echo -e "${BOLD}${CYAN}══════════════════════════════════════════════${RESET}\n"; } - -# ── Root check ──────────────────────────────────────────────────────────────── -[[ $EUID -eq 0 ]] || die "This script must be run as root (use: sudo bash imagehost-setup.sh)" - -# ── Detect AlmaLinux ───────────────────────────────────────────────────────── -if ! grep -qi 'almalinux' /etc/os-release 2>/dev/null; then - warn "This script is designed for AlmaLinux. Proceeding anyway, but results may vary." -fi - -# ============================================================================= -# INTERACTIVE CONFIGURATION -# ============================================================================= -banner "Image Host Setup — Configuration" - -# ── Domain ──────────────────────────────────────────────────────────────────── -echo -e "${BOLD}Domain name${RESET} (leave blank to use the server's IP address only):" -read -rp " Domain [none]: " DOMAIN -DOMAIN="${DOMAIN// /}" # strip spaces - -if [[ -n "$DOMAIN" ]]; then - echo -e "${BOLD}Email address${RESET} for Let's Encrypt certificate notices:" - read -rp " Email: " LE_EMAIL - LE_EMAIL="${LE_EMAIL// /}" - [[ "$LE_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]] || die "Invalid email address." - USE_SSL=true -else - USE_SSL=false - info "No domain provided — will serve over HTTP only." -fi - -# ── SFTP user ───────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}SFTP username${RESET} for image uploads (leave blank to use 'imageuser'):" -read -rp " Username [imageuser]: " SFTP_USER -SFTP_USER="${SFTP_USER:-imageuser}" -# Validate username -[[ "$SFTP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "Invalid username. Use lowercase letters, numbers, hyphens, underscores." - -echo "" -echo -e "${BOLD}SFTP password${RESET} (leave blank to auto-generate a strong password):" -read -rsp " Password [auto]: " SFTP_PASS -echo "" - -if [[ -z "$SFTP_PASS" ]]; then - SFTP_PASS="$(tr -dc 'A-Za-z0-9' >(tee -a "$LOG_FILE") 2>&1 - -# ============================================================================= -# STEP 1 — System update & packages -# ============================================================================= -banner "Step 1/7 — Installing packages" - -info "Updating system packages…" -dnf -y update --quiet - -info "Installing EPEL repository…" -dnf -y install epel-release --quiet - -info "Installing Nginx, fail2ban, certbot, and utilities…" -dnf -y install nginx fail2ban firewalld pwgen --quiet - -if $USE_SSL; then - dnf -y install certbot python3-certbot-nginx --quiet -fi - -success "Packages installed." - -# ============================================================================= -# STEP 2 — Create chroot jail + SFTP user -# ============================================================================= -banner "Step 2/7 — Creating SFTP user and chroot jail" - -# OpenSSH chroot requires the chroot root owned by root:root, mode 755 -info "Creating chroot directory structure…" -mkdir -p "${IMAGES_DIR}" -chown root:root "${CHROOT_DIR}" -chmod 755 "${CHROOT_DIR}" - -# The images subdirectory is owned by the SFTP user so they can write to it -if id "${SFTP_USER}" &>/dev/null; then - warn "User '${SFTP_USER}' already exists — skipping user creation." -else - info "Creating user '${SFTP_USER}'…" - useradd -r -s /sbin/nologin -d "${CHROOT_DIR}" -M "${SFTP_USER}" -fi - -echo "${SFTP_USER}:${SFTP_PASS}" | chpasswd - -chown "${SFTP_USER}:${SFTP_USER}" "${IMAGES_DIR}" -chmod 750 "${IMAGES_DIR}" - -success "User '${SFTP_USER}' created with chroot at ${CHROOT_DIR}." - -# ============================================================================= -# STEP 2b — Create maintenance user -# ============================================================================= -banner "Step 2b/7 — Creating maintenance user" - -echo -e "${BOLD}Maintenance username${RESET} for server administration (leave blank for 'siteadmin'):" -read -rp " Username [siteadmin]: " ADMIN_USER -ADMIN_USER="${ADMIN_USER:-siteadmin}" -[[ "$ADMIN_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "Invalid username." - -echo "" -echo -e "${BOLD}Maintenance password${RESET} (leave blank to auto-generate):" -read -rsp " Password [auto]: " ADMIN_PASS -echo "" - -if [[ -z "$ADMIN_PASS" ]]; then - ADMIN_PASS="$(tr -dc 'A-Za-z0-9' /dev/null; then - warn "User '${ADMIN_USER}' already exists — resetting password only." -else - info "Creating user '${ADMIN_USER}'…" - useradd -m -s /bin/bash "${ADMIN_USER}" -fi - -echo "${ADMIN_USER}:${ADMIN_PASS}" | chpasswd -usermod -aG wheel "${ADMIN_USER}" - -success "Maintenance user '${ADMIN_USER}' created and added to wheel (sudo) group." - - -# ============================================================================= -# STEP 3 — Harden SSH / configure chroot SFTP -# ============================================================================= -banner "Step 3/7 — Configuring SSH & SFTP chroot" - -SSHD_CONFIG="/etc/ssh/sshd_config" -SFTP_STANZA=" -# ── imagehost SFTP chroot ────────────────────────────────────────────────── -Match User ${SFTP_USER} - ChrootDirectory ${CHROOT_DIR} - ForceCommand internal-sftp - AllowTcpForwarding no - X11Forwarding no - PermitTunnel no - AllowAgentForwarding no -" - -# Remove any previous stanza for this user, then append the new one -if grep -q "Match User ${SFTP_USER}" "$SSHD_CONFIG"; then - info "Removing existing SSH stanza for '${SFTP_USER}'…" - # Use python for reliable multi-line deletion - python3 - "${SFTP_USER}" "$SSHD_CONFIG" <<'PYEOF' -import sys, re, pathlib -user = sys.argv[1] -path = pathlib.Path(sys.argv[2]) -text = path.read_text() -pattern = rf'\n# ── imagehost SFTP chroot ──.*?Match User {re.escape(user)}.*?AllowAgentForwarding no\n' -text = re.sub(pattern, '', text, flags=re.DOTALL) -path.write_text(text) -PYEOF -fi - -# Apply global hardening options FIRST (before the Match block) -apply_ssh_option() { - local key="$1" val="$2" - if grep -qiE "^\s*${key}\s" "$SSHD_CONFIG"; then - sed -i -E "s|^\s*${key}\s.*|${key} ${val}|i" "$SSHD_CONFIG" - else - # Insert before the first Match block, or append if no Match block exists - if grep -q "^Match " "$SSHD_CONFIG"; then - sed -i "/^Match /i ${key} ${val}" "$SSHD_CONFIG" - else - echo "${key} ${val}" >> "$SSHD_CONFIG" - fi - fi -} - -apply_ssh_option "PermitRootLogin" "prohibit-password" -apply_ssh_option "PasswordAuthentication" "yes" -apply_ssh_option "MaxAuthTries" "4" -apply_ssh_option "LoginGraceTime" "30" - -# Append the Match block LAST -printf '%s\n' "$SFTP_STANZA" >> "$SSHD_CONFIG" - -sshd -t || die "SSH config test failed — check ${SSHD_CONFIG}" -systemctl restart sshd -success "SSH/SFTP configured." - -# ============================================================================= -# STEP 4 — Nginx configuration -# ============================================================================= -banner "Step 4/7 — Configuring Nginx" - -# Determine server_name -if [[ -n "$DOMAIN" ]]; then - SERVER_NAME="$DOMAIN" -else - # Use the primary non-loopback IP - SERVER_NAME=$(hostname -I | awk '{print $1}') -fi - -cat > "$NGINX_CONF" </dev/null && command -v restorecon &>/dev/null; then - semanage fcontext -a -t httpd_sys_content_t "${CHROOT_DIR}(/.*)?" 2>/dev/null || \ - semanage fcontext -m -t httpd_sys_content_t "${CHROOT_DIR}(/.*)?." 2>/dev/null || true - restorecon -Rv "${CHROOT_DIR}" >/dev/null - # Allow Nginx to read the chroot dir - setsebool -P httpd_read_user_content 1 2>/dev/null || true - success "SELinux contexts applied." -else - warn "semanage not found — skipping SELinux context setup (may not be needed)." -fi - -# ============================================================================= -# STEP 5 — Firewall -# ============================================================================= -banner "Step 5/7 — Configuring firewall" - -systemctl enable --now firewalld - -firewall-cmd --quiet --permanent --add-service=ssh -firewall-cmd --quiet --permanent --add-service=http -$USE_SSL && firewall-cmd --quiet --permanent --add-service=https -firewall-cmd --quiet --reload -success "Firewall rules applied (SSH + HTTP${USE_SSL:+ + HTTPS})." - -# ============================================================================= -# STEP 6 — fail2ban -# ============================================================================= -banner "Step 6/7 — Configuring fail2ban" - -cat > /etc/fail2ban/jail.d/imagehost.conf </dev/null || \ - (crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet") | crontab - - else - warn "Certbot failed — falling back to HTTP." - warn "Make sure your domain's DNS A record points to this server's IP." - warn "You can re-run certbot manually later:" - warn " certbot --nginx -d ${DOMAIN} --email ${LE_EMAIL} --agree-tos --redirect" - USE_SSL=false - fi -else - banner "Step 7/7 — SSL skipped (no domain provided)" - info "Serving over HTTP only." -fi - -# ============================================================================= -# FINAL SUMMARY -# ============================================================================= -banner "Setup Complete!" - -# Determine the base URL -if $USE_SSL; then - BASE_URL="https://${DOMAIN}" -elif [[ -n "$DOMAIN" ]]; then - BASE_URL="http://${DOMAIN}" -else - IP=$(hostname -I | awk '{print $1}') - BASE_URL="http://${IP}" -fi - -echo -e "${BOLD}Your image host is ready.${RESET}" -echo "" -echo -e " ${BOLD}Image URL format:${RESET}" -echo -e " ${CYAN}${BASE_URL}/${RESET}" -echo "" -echo -e " ${BOLD}SFTP connection details:${RESET}" -echo -e " Host : ${CYAN}${DOMAIN:-$(hostname -I | awk '{print $1}')}${RESET}" -echo -e " Port : ${CYAN}22${RESET}" -echo -e " Username : ${CYAN}${SFTP_USER}${RESET}" -if $GENERATED_PASS; then -echo -e " Password : ${YELLOW}${SFTP_PASS}${RESET} ${RED}← Save this now!${RESET}" -else -echo -e " Password : ${CYAN}(as you entered)${RESET}" -fi -echo -e " Upload to: ${CYAN}/images/${RESET} (this is the root you'll see in your SFTP client)" -echo "" -echo -e " ${BOLD}Maintenance (SSH) login:${RESET}" -echo -e " Host : ${CYAN}${DOMAIN:-$(hostname -I | awk '{print $1}')}${RESET}" -echo -e " Port : ${CYAN}22${RESET}" -echo -e " Username : ${CYAN}${ADMIN_USER}${RESET}" -if $GENERATED_ADMIN_PASS; then -echo -e " Password : ${YELLOW}${ADMIN_PASS}${RESET} ${RED}← Save this now!${RESET}" -else -echo -e " Password : ${CYAN}(as you entered)${RESET}" -fi -echo -e " Sudo : ${CYAN}sudo -i${RESET} to become root" -echo "" -echo -e " ${BOLD}Allowed file types:${RESET} JPG, PNG, GIF, WebP, AVIF, SVG, BMP, TIFF" -echo -e " ${BOLD}Max file size:${RESET} ${MAX_SIZE_MB} MB" -echo "" -echo -e " ${BOLD}Recommended SFTP clients:${RESET}" -echo -e " • FileZilla (Windows / Mac / Linux) — free" -echo -e " • Cyberduck (Windows / Mac) — free" -echo -e " • WinSCP (Windows) — free" -echo "" -echo -e " ${BOLD}Setup log:${RESET} ${LOG_FILE}" -echo "" -echo -e "${GREEN}${BOLD}All done!${RESET}"