Fix markdown lint issues in README (107 IDE warnings -> 0)
- Auto-fix MD060 table column style, MD012 multiple blanks, MD031/MD032 list/fence spacing - Add language tags to 4 bare code fences (MD040) - Add .markdownlint.json disabling MD013 (wide tables/commands are intentional) - Verified: markdownlint-cli2 reports 0 issues across README and docs/
This commit is contained in:
parent
1a999c7548
commit
7523c882a6
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
// Line length is not enforced: this README uses wide tables and long
|
||||
// inline-command lines that would be unreadable if wrapped.
|
||||
"MD013": false
|
||||
}
|
||||
53
README.md
53
README.md
|
|
@ -36,7 +36,7 @@ Work proceeds in two phases: `--bootstrap` hardens the host and stands up the pl
|
|||
**Solution:** `wp-engine.sh` performs two phases:
|
||||
|
||||
| Phase | Command | Purpose |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| **Phase 1 — Host Hardening & Platform** | `./wp-engine.sh --bootstrap` | Installs packages, container runtime, hardened SSH, UFW firewall, Fail2ban jails, container networks, ingress reverse proxy, and the Grafana/Prometheus monitoring stack. |
|
||||
| **Phase 2 — Site Provisioning** | `./wp-engine.sh create <slug> <domain> ...` | Creates an isolated WordPress site (container, database, DB user, Redis cache index, TLS route, WP-CLI automated install). |
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ Supporting design documents live in [`docs/`](docs/):
|
|||
## 2. Requirements
|
||||
|
||||
| Requirement | Detail |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| **OS** | Clean Debian 11/12 or Ubuntu 22.04/24.04 LTS installation (bare-metal, Proxmox VM, or Proxmox LXC) with `apt` and `systemd` (the script calls `apt-get`, `systemctl`, `ufw`). Not compatible with RHEL/Alpine/Windows hosts. |
|
||||
| **Privileges** | Must be run as `root` (the script exits if `EUID != 0`). |
|
||||
| **Network** | Outbound internet access to pull packages and container images (Docker Hub, download.docker.com, etc.). |
|
||||
|
|
@ -105,6 +105,7 @@ chmod +x /usr/local/bin/wp-engine.sh
|
|||
|
||||
> [!WARNING]
|
||||
> **Do not close your current SSH terminal** after running `--bootstrap`. Open a secondary terminal and verify that you can connect over your chosen port before disconnecting:
|
||||
>
|
||||
> ```bash
|
||||
> ssh -p <PORT> user@<HOST_IP>
|
||||
> ```
|
||||
|
|
@ -113,7 +114,7 @@ chmod +x /usr/local/bin/wp-engine.sh
|
|||
|
||||
## 4. CLI Reference & Options
|
||||
|
||||
```
|
||||
```text
|
||||
Usage:
|
||||
Interactive Bootstrap:
|
||||
./wp-engine.sh --bootstrap
|
||||
|
|
@ -132,7 +133,7 @@ Examples:
|
|||
### Actions
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| `--bootstrap` | Runs the 6-step host provisioning sequence, then deploys the monitoring stack. Prompts for any option not supplied via flags. |
|
||||
| `create <slug> <domain> [admin_user] [admin_email]` | Provisions a new WordPress site. If the ingress proxy isn't running yet, bootstrap runs automatically; if the shared DB/cache core isn't up, it is initialized automatically. |
|
||||
| *(no/unknown action)* | Prints the usage text above and exits with code 1. |
|
||||
|
|
@ -140,7 +141,7 @@ Examples:
|
|||
### Flags (used with `--bootstrap`)
|
||||
|
||||
| Flag | Values | Default | Effect |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| `--ssh-port` | `1–65535` | `48222` | Moves SSH off port 22; written to `sshd_config.d/99-hardened.conf`, the `ssh.socket` unit (if present), UFW, and the Fail2ban `sshd` jail. |
|
||||
| `--engine` | `docker` \| `podman` | `docker` | Container runtime. Docker installs from Docker's official apt repo; Podman installs `podman`, `podman-compose`, `podman-docker` from distro repos. |
|
||||
| `--proxy` | `npm` \| `traefik` \| `caddy` | `npm` | Ingress reverse proxy. Determines the proxy compose file, extra firewall openings, TLS automation, and how sites get routed. |
|
||||
|
|
@ -150,7 +151,7 @@ Examples:
|
|||
### `create` arguments
|
||||
|
||||
| Position | Argument | Default | Description |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `slug` | *(required)* | Directory/container name, e.g. `teetimeprints`. Hyphens are converted to underscores for DB identifiers. |
|
||||
| 2 | `domain` | *(required)* | Public FQDN, e.g. `teetimeprints.com`. Used for TLS routing and the WP site URL. |
|
||||
| 3 | `admin_user` | `admin` | WordPress administrator username. |
|
||||
|
|
@ -176,13 +177,14 @@ Additional `create` examples:
|
|||
Installs `apt-transport-https ca-certificates curl gnupg lsb-release ufw fail2ban nftables openssl jq`.
|
||||
|
||||
**`[2/6] Setting up Container Engine`**
|
||||
|
||||
- *Docker:* adds Docker's official GPG key + apt repo, installs `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`, `docker-compose-plugin`, enables the service (skipped if Docker already present).
|
||||
- *Podman:* `apt-get install podman podman-compose podman-docker`, enables the service.
|
||||
|
||||
**`[3/6] Hardening SSH on Port <port>`**
|
||||
Writes `/etc/ssh/sshd_config.d/99-hardened.conf`:
|
||||
|
||||
```
|
||||
```text
|
||||
Port <chosen port>
|
||||
PermitRootLogin prohibit-password
|
||||
PasswordAuthentication yes
|
||||
|
|
@ -195,12 +197,14 @@ ClientAliveCountMax 2
|
|||
Also re-points `ssh.socket`'s `ListenStream` to the new port when socket activation is in use, then restarts SSH.
|
||||
|
||||
**`[4/6] Configuring Firewall & Isolation Rules`**
|
||||
|
||||
- UFW policy: **deny incoming / allow outgoing**.
|
||||
- Allowed inbound: SSH port, `80`, `443`, `3000` (Grafana), plus `81` if NPM or `8080` if Traefik.
|
||||
- For Docker, appends a **`DOCKER-USER` chain** to `/etc/ufw/after.rules` so UFW rules actually apply to container traffic (blocks direct SYN scans into `172.16.0.0/12`, rate-limited logging).
|
||||
- `ufw --force enable && ufw reload`.
|
||||
|
||||
**`[5/6] Scaffolding Networks & Fail2ban Jails`**
|
||||
|
||||
- Creates the directory skeleton (`/server/websites/{proxy,core,template,sites,monitoring}`).
|
||||
- Creates three external container networks: `proxy-net`, `shared-backend`, `monitoring-net`.
|
||||
- Writes Fail2ban jails:
|
||||
|
|
@ -215,7 +219,7 @@ Also re-points `ssh.socket`'s `ListenStream` to the new port when socket activat
|
|||
Deploys `/server/websites/monitoring/docker-compose.yml` on the isolated `monitoring-net`:
|
||||
|
||||
| Service | Image | Purpose | Exposure |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| `prometheus` | `prom/prometheus:v2.54.0` | Time-series DB; scrapes node_exporter + cAdvisor every 15 s | internal |
|
||||
| `node_exporter` | `prom/node-exporter:v1.8.2` | Host CPU/RAM/disk metrics (`/proc`, `/sys`, `/` mounted read-only) | internal |
|
||||
| `cadvisor` | `gcr.io/cadvisor/cadvisor:v0.49.1` | Per-container cgroup resource usage | internal |
|
||||
|
|
@ -264,7 +268,7 @@ A random Grafana admin password is generated (`openssl rand`) and appended to th
|
|||
### 7.1 Container runtime
|
||||
|
||||
| | **Docker** (default) | **Podman** |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| Install source | Docker official apt repo (`docker-ce` + compose plugin) | Distro repos (`podman`, `podman-compose`, `podman-docker`) |
|
||||
| Compose command | `docker compose` | `podman-compose` |
|
||||
| UFW integration | Gets the `DOCKER-USER` after.rules patch (required, since Docker bypasses UFW by default) | Not applied (Podman networking works differently with nftables/UFW) |
|
||||
|
|
@ -273,7 +277,7 @@ A random Grafana admin password is generated (`openssl rand`) and appended to th
|
|||
### 7.2 Ingress proxy
|
||||
|
||||
| | **NPM** (default) | **Traefik v3** | **Caddy** |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| Image | `jc21/nginx-proxy-manager` | `traefik:v3.1` | `caddy:2-alpine` |
|
||||
| Management | Web UI on **`:81`** (opened in UFW) | Docker labels + insecure API dashboard on **`:8080`** (opened in UFW) | Text `Caddyfile` |
|
||||
| TLS certificates | Requested per host in the UI (Let's Encrypt) | ACME **TLS-ALPN challenge** via `myresolver` labels | **Fully automatic** Let's Encrypt |
|
||||
|
|
@ -329,7 +333,7 @@ High-level traffic flow (see `docs/` for the full Mermaid diagrams):
|
|||
### Networks
|
||||
|
||||
| Network | Scope | Members |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `proxy-net` | external | ingress proxy + all WordPress containers (public-facing side) |
|
||||
| `shared-backend` | external, isolated | all WordPress containers + `core_mariadb` + `core_redis` (no host ports published) |
|
||||
| `monitoring-net` | external, isolated | Prometheus, node_exporter, cAdvisor, Grafana |
|
||||
|
|
@ -337,7 +341,7 @@ High-level traffic flow (see `docs/` for the full Mermaid diagrams):
|
|||
### Ports
|
||||
|
||||
| Port | Service | Firewall |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| 22 → **48222** (default) | SSH | allowed (custom port) |
|
||||
| 80 / 443 | Ingress proxy (HTTP/HTTPS + ACME) | allowed |
|
||||
| 81 | Nginx Proxy Manager admin UI | allowed **only if** `--proxy npm` |
|
||||
|
|
@ -347,7 +351,7 @@ High-level traffic flow (see `docs/` for the full Mermaid diagrams):
|
|||
|
||||
### Filesystem layout (`/server/websites/`)
|
||||
|
||||
```
|
||||
```text
|
||||
/server/websites/
|
||||
├── .engine-settings # Persisted choices: ENGINE, PROXY_TYPE, SSH_PORT, GRAFANA_ADMIN_PASS
|
||||
├── core/ # Shared data tier
|
||||
|
|
@ -384,7 +388,7 @@ Host-level configs written by the script:
|
|||
## 9. Credentials & State Files
|
||||
|
||||
| Secret | Where it lives | Notes |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| Engine / proxy / SSH port | `/server/websites/.engine-settings` | plain, non-secret |
|
||||
| Grafana admin password | `/server/websites/.engine-settings` → `GRAFANA_ADMIN_PASS` | login: `admin` @ `http://<host>:3000` |
|
||||
| MariaDB **root** password | `/server/websites/core/.env` → `DB_ROOT_PASSWORD` | `chmod 600`; used for creating per-site DBs |
|
||||
|
|
@ -403,13 +407,15 @@ After bootstrap:
|
|||
|
||||
1. Open `http://<HOST_IP>:3000` (Grafana).
|
||||
2. Log in with `admin` / the password generated during bootstrap and saved in the state file:
|
||||
|
||||
```bash
|
||||
grep GRAFANA_ADMIN_PASS /server/websites/.engine-settings
|
||||
```
|
||||
|
||||
3. The **Prometheus** datasource is pre-provisioned and set as default. Import dashboards via **Dashboards → Import** — recommended IDs:
|
||||
|
||||
| ID | Dashboard | Provides |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `1860` | Node Exporter Full | Real-time CPU, RAM, disk I/O, network throughput, load averages |
|
||||
| `14282` | cAdvisor Docker Monitoring | Per-container resource usage; spot CPU/RAM throttling across tenant sites |
|
||||
|
||||
|
|
@ -467,7 +473,7 @@ fail2ban-client get ingress-wordpress banned
|
|||
## 12. Security Hardening Summary
|
||||
|
||||
| Layer | Control |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| **Access** | SSH moved off port 22; root login limited to key auth (`PermitRootLogin prohibit-password`); `MaxAuthTries 4`; X11 forwarding off; idle timeout via `ClientAliveInterval`/`ClientAliveCountMax` |
|
||||
| **Perimeter** | UFW default-deny inbound; explicit allow-list (SSH, 80, 443, 3000, +81/8080 per proxy) |
|
||||
| **Container escape path** | `DOCKER-USER` nftables chain in `/etc/ufw/after.rules` so UFW governs Docker-bridged traffic; direct scans into RFC1918 container ranges are logged + dropped |
|
||||
|
|
@ -503,7 +509,7 @@ The script expects **root + systemd + apt + a privileged container runtime**, so
|
|||
### 14.1 Test stages — `tests/smoke.sh`
|
||||
|
||||
| `STAGE=` | Needs | What it verifies |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `static` *(default)* | Linux (WSL is fine), **no root** | `bash -n` syntax on both scripts, `shellcheck -S error`, usage/argument handling, non-root rejection via `setpriv` |
|
||||
| `bootstrap` | root + systemd + privileged container | Full unattended `--bootstrap` (Docker + Caddy), then asserts: state file, UFW active, both Fail2ban jails, all 5 containers running, Grafana health, proxy on :80, SSH on the hardened port |
|
||||
| `site` | same as `bootstrap` | Auto-bootstraps if needed, runs `create smoketest smoketest.test`, then asserts: site dir, WP container, `wp-config.php`, scoped DB `wp_smoketest`, Caddyfile route, HTTP response through the proxy, `wp core is-installed` |
|
||||
|
|
@ -608,10 +614,7 @@ Harness fixes (in `tests/smoke.sh`): Grafana/proxy assertions got 60 s retry win
|
|||
|
||||
## Repository Contents
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
```text
|
||||
wp-engine.sh # The installer/provisioner script (executable)
|
||||
README.md # This file
|
||||
.gitignore / .gitattributes # Secrets excluded; *.sh forced to LF
|
||||
|
|
@ -624,11 +627,3 @@ docs/
|
|||
│ Runtime_Architecture_Network_Topology.md # Architecture & network Mermaid diagram
|
||||
│ Filesystem_and_Directory_Structure.md # Filesystem layout Mermaid diagram
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue