Added expanded README.md (still need video link)
This commit is contained in:
parent
c8fcd5beb5
commit
e7bc3decdc
234
README.md
234
README.md
@ -1,30 +1,234 @@
|
|||||||
# imagehost-setup
|
# imagehost-setup
|
||||||
|
|
||||||
A single-script installer that turns a fresh AlmaLinux 10 VPS into a
|
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.
|
||||||
lightweight, secure image host — SFTP upload, Nginx serving, optional
|
|
||||||
Let's Encrypt SSL, and fail2ban out of the box.
|
|
||||||
|
|
||||||
## Requirements
|
## 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
|
- 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
|
The script will walk you through the rest interactively. When it finishes, it prints your SFTP credentials and image URL — save them somewhere safe.
|
||||||
sudo bash imagehost-setup.sh
|
|
||||||
|
|
||||||
The script will walk you through the rest interactively.
|
|
||||||
|
|
||||||
## What it sets up
|
## What it sets up
|
||||||
|
|
||||||
- **Nginx** — serves image files only (JPG, PNG, GIF, WebP, AVIF, SVG, BMP, TIFF)
|
- **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 with no shell access
|
- **SFTP chroot** — a locked-down upload user that can only access the images folder; no shell access
|
||||||
- **firewalld** — opens only SSH, HTTP, and HTTPS
|
- **Maintenance user** — a normal SSH login with sudo access for administration
|
||||||
- **fail2ban** — brute-force protection on SSH and Nginx
|
- **firewalld** — opens only SSH, HTTP, and HTTPS; everything else is blocked
|
||||||
- **Let's Encrypt SSL** — optional, with HTTP fallback
|
- **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/<filename.jpg>
|
||||||
|
|
||||||
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -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' </dev/urandom | head -c 24 || true)"
|
|
||||||
GENERATED_PASS=true
|
|
||||||
else
|
|
||||||
GENERATED_PASS=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Max upload file size ──────────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}Maximum image file size${RESET} in megabytes (leave blank for 20 MB):"
|
|
||||||
read -rp " Max size MB [20]: " MAX_SIZE_MB
|
|
||||||
MAX_SIZE_MB="${MAX_SIZE_MB:-20}"
|
|
||||||
[[ "$MAX_SIZE_MB" =~ ^[0-9]+$ ]] || die "Max size must be a whole number."
|
|
||||||
|
|
||||||
# ── Confirm ───────────────────────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo -e "${BOLD}──────── Summary ────────${RESET}"
|
|
||||||
echo -e " SFTP username : ${CYAN}${SFTP_USER}${RESET}"
|
|
||||||
if $GENERATED_PASS; then
|
|
||||||
echo -e " SFTP password : ${YELLOW}(auto-generated — shown at the end)${RESET}"
|
|
||||||
else
|
|
||||||
echo -e " SFTP password : ${CYAN}(as entered)${RESET}"
|
|
||||||
fi
|
|
||||||
echo -e " Domain : ${CYAN}${DOMAIN:-'(none — IP only)'}${RESET}"
|
|
||||||
echo -e " SSL : ${CYAN}${USE_SSL}${RESET}"
|
|
||||||
echo -e " Max file size : ${CYAN}${MAX_SIZE_MB} MB${RESET}"
|
|
||||||
echo ""
|
|
||||||
read -rp "Proceed with installation? [y/N] " CONFIRM
|
|
||||||
[[ "${CONFIRM,,}" == "y" ]] || { echo "Aborted."; exit 0; }
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DERIVED VARIABLES
|
|
||||||
# =============================================================================
|
|
||||||
CHROOT_DIR="/srv/imagehost"
|
|
||||||
IMAGES_DIR="${CHROOT_DIR}/images"
|
|
||||||
NGINX_CONF="/etc/nginx/conf.d/imagehost.conf"
|
|
||||||
MAX_SIZE_NGINX="${MAX_SIZE_MB}m"
|
|
||||||
LOG_FILE="/var/log/imagehost-setup.log"
|
|
||||||
|
|
||||||
# All output also goes to a log file
|
|
||||||
exec > >(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/urandom | head -c 24 || true)"
|
|
||||||
GENERATED_ADMIN_PASS=true
|
|
||||||
else
|
|
||||||
GENERATED_ADMIN_PASS=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if id "${ADMIN_USER}" &>/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" <<NGINXCONF
|
|
||||||
# imagehost — generated by imagehost-setup.sh
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name ${SERVER_NAME};
|
|
||||||
|
|
||||||
root ${IMAGES_DIR};
|
|
||||||
autoindex off;
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
client_max_body_size ${MAX_SIZE_NGINX};
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "DENY" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
|
||||||
add_header Content-Security-Policy "default-src 'none'; img-src 'self'" always;
|
|
||||||
|
|
||||||
# Allow only image file types
|
|
||||||
location ~* \.(jpe?g|png|gif|webp|avif|svg|ico|bmp|tiff?)$ {
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Block everything else
|
|
||||||
location / {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hide dot-files
|
|
||||||
location ~ /\. {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
access_log /var/log/nginx/imagehost_access.log;
|
|
||||||
error_log /var/log/nginx/imagehost_error.log;
|
|
||||||
}
|
|
||||||
NGINXCONF
|
|
||||||
|
|
||||||
# Remove default Nginx config if present
|
|
||||||
[[ -f /etc/nginx/conf.d/default.conf ]] && mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.bak
|
|
||||||
|
|
||||||
nginx -t || die "Nginx config test failed — check ${NGINX_CONF}"
|
|
||||||
systemctl enable --now nginx
|
|
||||||
success "Nginx configured and started."
|
|
||||||
|
|
||||||
# Nginx needs read access to the images directory (SELinux-aware)
|
|
||||||
info "Setting file contexts for SELinux…"
|
|
||||||
if command -v semanage &>/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 <<F2B
|
|
||||||
[DEFAULT]
|
|
||||||
bantime = 1h
|
|
||||||
findtime = 10m
|
|
||||||
maxretry = 5
|
|
||||||
backend = systemd
|
|
||||||
|
|
||||||
[sshd]
|
|
||||||
enabled = true
|
|
||||||
port = ssh
|
|
||||||
logpath = %(sshd_log)s
|
|
||||||
|
|
||||||
[nginx-http-auth]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[nginx-limit-req]
|
|
||||||
enabled = true
|
|
||||||
F2B
|
|
||||||
|
|
||||||
systemctl enable --now fail2ban
|
|
||||||
success "fail2ban configured and started."
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# STEP 7 — Let's Encrypt SSL (optional)
|
|
||||||
# =============================================================================
|
|
||||||
if $USE_SSL; then
|
|
||||||
banner "Step 7/7 — Obtaining SSL certificate"
|
|
||||||
info "Requesting certificate for ${DOMAIN}…"
|
|
||||||
|
|
||||||
if certbot --nginx \
|
|
||||||
--staging \
|
|
||||||
--non-interactive \
|
|
||||||
--agree-tos \
|
|
||||||
--email "$LE_EMAIL" \
|
|
||||||
--domains "$DOMAIN" \
|
|
||||||
--redirect; then
|
|
||||||
success "SSL certificate installed. HTTPS enabled."
|
|
||||||
# Ensure auto-renewal is active
|
|
||||||
systemctl enable --now certbot-renew.timer 2>/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}/<filename.jpg>${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}"
|
|
||||||
Loading…
Reference in New Issue
Block a user