diff --git a/.gitignore b/.gitignore index f8b9f34..8cdf339 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ go.work.sum .env # Project specific -fail2ban-ui-settings.json \ No newline at end of file +fail2ban-ui-settings.json +_dev \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ddda406 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# ========================================= +# STAGE 1: Build Fail2Ban UI Binary +# ========================================= +FROM golang:1.22.9 AS builder + +WORKDIR /app + +# Copy module files and download dependencies first +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the application source code +COPY . . + +# Build Go application (as static binary) +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o fail2ban-ui ./cmd/server/main.go + +# =================================== +# STAGE 2: Standalone UI Version +# =================================== +FROM alpine:latest AS standalone-ui + +# Install required container dependencies +RUN apk --update --no-cache add \ + bash curl wget whois tzdata jq ca-certificates htop fail2ban geoip \ + && adduser -D -u 1000 -G root fail2ban + +RUN mkdir -p /app /config \ + /etc/fail2ban/jail.d \ + /etc/fail2ban/filter.d \ + /etc/fail2ban/action.d \ + /var/run/fail2ban \ + /usr/share/GeoIP \ + && touch /etc/fail2ban/jail.local \ + && chown -R fail2ban:0 /app /config /etc/fail2ban /var/run/fail2ban + +# Set working directory +WORKDIR /config + +# Copy Fail2Ban UI binary and templates from the build stage +COPY --from=builder /app/fail2ban-ui /app/fail2ban-ui +RUN chown fail2ban:0 /app/fail2ban-ui && chmod +x /app/fail2ban-ui +COPY --from=builder /app/pkg/web/templates /app/templates + +# Set environment variables +ENV CONTAINER=true + +# Persist config data +VOLUME ["/config"] + +# Expose UI port +EXPOSE 8080 + +# Run the application as non-root (currently not possible because of fail2ban running as privileged) +#USER fail2ban + +# Start Fail2Ban UI +CMD ["/app/fail2ban-ui"] diff --git a/README.md b/README.md index 328cd55..3adb458 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,133 @@ -# Fail2ban UI +# **Fail2Ban UI** -A Swissmade, management interface for [Fail2ban](https://www.fail2ban.org/). -It provides a modern dashboard to currently: +πŸš€ **Fail2Ban-UI** is a Swiss-made **web-based management interface** for [Fail2Ban](https://www.fail2ban.org/). +It provides an intuitive dashboard to **monitor, configure, and manage Fail2Ban** in real time. -- View all Fail2ban jails and banned IPs -- Unban IP addresses directly -- Edit and save jail/filter configs -- Reload Fail2ban when needed -- See recent ban events -- More to come... +Developed by **[Swissmakers GmbH](https://swissmakers.ch)**. -Built by [Swissmakers GmbH](https://swissmakers.ch). +## **✨ Features** + +βœ… **Real-time Dashboard** +- View **all active Fail2Ban jails** and **banned IPs** in a clean UI +- Displays **live ban events** + +βœ… **Ban & Unban Management** +- **Unban IPs** directly via the UI +- **Search** for banned IPs accross all active jails + +βœ… **Fail2Ban Configuration Management** +- **Edit & Save** active Fail2Ban jail/filter configs +- Get automatic **email alerts** for specific country-based bans +- Configure own SMTP settings for email alerts (STARTTLS only) +- Adjust default ban time, find time, and set ignore IPs +- Auto-detects changes and prompts for **reload** to apply +- Enable debug-mode for detailed module logs + +βœ… **Mobile-Friendly & Responsive UI / Fast** +- Optimized for **mobile & desktop** +- Powered by **Bootstrap 5** +- **Go-based backend** ensures minimal resource usage + +βœ… **Systemd & SELinux Support** +- **Run as a systemd service** (Standalone or Container) +- **Supports SELinux** for secure execution (also container version) + +## **πŸ“Έ Screenshots** +Some images from the UI in action: + +| Dashboard | Search | Filter Configuration | +|-----------|-------------|--------------------| +| ![Dashboard](./screenshots/0_Dashboard.jpg) | ![Filter Debug](./screenshots/1_Dashboard_search.jpg) | ![Jail Config](./screenshots/3_Dashboard_edit_filter.jpg) | + +πŸ“Œ **More screenshots are found [here](./screenshots/)** --- -## Features +## **πŸ“₯ Installation & Deployment** -1. **Basic Real-time Dashboard** - - Automatically loads all jails, banned IPs, and last 5 ban events on page load. +Fail2Ban-UI can be currently deployed in **two main ways**: +**1️⃣ Running from local source** +**2️⃣ Running as a container** -2. **Unban IPs** - - Unban any blocked IP without needing direct CLI access. +### **πŸ”Ή Method 1: Running from Local Source** +To install and run directly on the system: +πŸ“Œ **[Follow the basic systemd setup guide](./deployment/systemd/README.md)** -3. **Edit Fail2ban Configs** - - Click on any jail name to open a modal with raw config contents (from `/etc/fail2ban/filter.d/*.conf` by default). - - Save changes, then reload Fail2ban. - -4. **Responsive UI** - - Built with [Bootstrap 5](https://getbootstrap.com/). - -5. **Loading Overlay & Reload Banner** - - Displays a loading spinner for all operations. - - Shows a reload banner when configuration changes occur. +```bash +git clone https://github.com/swissmakers/fail2ban-ui.git /opt/fail2ban-ui +cd /opt/fail2ban-ui +go build -o fail2ban-ui ./cmd/main.go +... +``` --- -## Requirements +### **πŸ”Ή Method 2: Running as a Container** +For an easy containerized deployment: +πŸ“Œ **[Follow the basic container deployment guide](./deployment/container/README.md)** -- **Go 1.22.9+** (module-compatible) -- **Fail2ban** installed and running -- **Linux** environment with permissions to run `fail2ban-client` and read/write config files (e.g., `/etc/fail2ban/filter.d/`) -- Sufficient privileges to reload Fail2ban (run as `sudo` or configure your system accordingly) +```bash +podman run -d \ + --name fail2ban-ui \ + --network=host \ + -v /opt/podman-fail2ban-ui:/config:Z \ + -v /etc/fail2ban:/etc/fail2ban:Z \ + -v /var/log:/var/log:ro \ + -v /var/run/fail2ban:/var/run/fail2ban \ + -v /usr/share/GeoIP:/usr/share/GeoIP:ro \ + localhost/fail2ban-ui +``` ---- +> **πŸ“Œ Note:** The container can also be managed as a **systemd service**. -## Installation & Usage -1. **Clone the repository**: - ```bash - git clone https://github.com/swissmakers/fail2ban-ui.git - cd fail2ban-ui - ``` +## **πŸ”’ Security Considerations** +- Fail2Ban-UI requires **root privileges** to interact with Fail2Ban. +- **Restrict access** using **firewall rules** or a **reverse proxy** with authentication. +- Ensure that Fail2Ban logs/configs **aren't exposed publicly**. -2. **Initialize or tidy Go modules** (optional if you already have them): - ```bash - go mod tidy - ``` +For **SELinux users**, apply the **Fail2Ban-UI security policies**: +```bash +# Basic rule to allow fail2ban access the fail2ban-ui API +semodule -i fail2ban-curl-allow.pp +# Also needed for a secure container deployment +semodule -i fail2ban-container-ui.pp +semodule -i fail2ban-container-client.pp +``` -3. **Run the server** (with `sudo` if necessary): - ```bash - sudo go run ./cmd/server - ``` - By default, it listens on port `:8080`. -4. **Open the UI**: - - Visit [http://localhost:8080/](http://localhost:8080/) (or replace `localhost` with your server IP). +## **πŸ› οΈ Troubleshooting** -5. **Manage Fail2ban**: - - See jails and banned IPs on the main dashboard - - Unban IPs via the β€œUnban” button - - Edit jail configs by clicking the jail name - - Save your changes, then **reload** Fail2ban using the top banner prompt +### **UI not accessible?** +- Ensure **port 8080** is open: + ```bash + sudo firewall-cmd --add-port=8080/tcp --permanent + sudo firewall-cmd --reload + ``` +- Check logs: + ```bash + journalctl -u fail2ban-ui.service -f + ``` ---- - -## Security Considerations - -- Running this UI typically requires **root** or sudo privileges to execute `fail2ban-client` and manipulate config files. -- Consider restricting network access or using authentication (e.g., reverse proxy with Basic Auth or a firewall rule) to ensure only authorized users can access the dashboard. -- Make sure your Fail2ban logs and configs aren’t exposed publicly. - ---- - -## Contributing - -We welcome pull requests and issues! Please open an [issue](./issues) if you find a bug or have a feature request. +## **🀝 Contributing** +We welcome **pull requests** and **feature suggestions**! 1. **Fork** this repository -2. **Create** a new branch: `git checkout -b feature/my-feature` -3. **Commit** your changes: `git commit -m 'Add some feature'` -4. **Push** to the branch: `git push origin feature/my-feature` -5. **Open** a pull request +2. **Create** a new branch: + ```bash + git checkout -b feature/my-feature + ``` +3. **Commit** your changes: + ```bash + git commit -m "Add new feature" + ``` +4. **Push** to the branch: + ```bash + git push origin feature/my-feature + ``` +5. **Open** a Pull Request ---- -## License - -```text -GNU GENERAL PUBLIC LICENSE, Version 3 -``` \ No newline at end of file +## **πŸ“œ License** +Fail2Ban-UI is licensed under **GNU GENERAL PUBLIC LICENSE, Version 3**. +See [`LICENSE`](./LICENSE) for details. \ No newline at end of file diff --git a/cmd/server/main.go b/cmd/server/main.go index 9c5ec89..05d5889 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "os" "time" "github.com/gin-gonic/gin" @@ -21,8 +22,15 @@ func main() { } router := gin.Default() - router.LoadHTMLGlob("pkg/web/templates/*") // Load HTML templates from pkg/web/templates - web.RegisterRoutes(router) // Register routes (IndexHandler, /api/summary, jail/unban/:ip) etc.. + + // To detect if running inside a container or not + _, container := os.LookupEnv("CONTAINER") + if container { + router.LoadHTMLGlob("/app/templates/*") // Load HTML templates + } else { + router.LoadHTMLGlob("pkg/web/templates/*") // Load HTML templates + } + web.RegisterRoutes(router) printWelcomeBanner() log.Println("--- Fail2Ban-UI started in", gin.Mode(), "mode ---") diff --git a/deployment/container/README.md b/deployment/container/README.md new file mode 100644 index 0000000..36904d8 --- /dev/null +++ b/deployment/container/README.md @@ -0,0 +1,88 @@ +# **Fail2Ban-UI Container** + +A **containerized version of Fail2Ban-UI**, allowing easy deployment for managing Fail2Ban configurations, logs, and bans via a web-based UI. + + +## How to Build the Image + +```bash +podman build -t fail2ban-ui --target=standalone-ui . +``` + +For **Docker**, just replace `podman` with `docker` for every command, e.g.: +```bash +docker build -t fail2ban-ui --target=standalone-ui . +``` + + +## For SELinux enabled systems +If SELinux is enabled, you must apply the required SELinux policies to allow the container to communicate with Fail2Ban. +The policies are located here: "`./SELinux/`" + +Apply the prebuilt SELinux Modules with: + +```bash +semodule -i fail2ban-container-ui.pp +semodule -i fail2ban-container-client.pp +``` + +### Manually Compile and Install SELinux Rules + +If you want to change or compile the SELinux rules by yourself run: + +```bash +checkmodule -M -m -o fail2ban-container-client.mod fail2ban-container-client.te +semodule_package -o fail2ban-container-client.pp -m fail2ban-container-client.mod +semodule -i fail2ban-container-client.pp +``` + + +## How to Run the Container + +Create the needed folder to store the fail2ban-ui config first: +```bash +mkdir /opt/podman-fail2ban-ui +``` + +Then run the container with the following prompt in background (-d) as test. For a productive container setup please use a systemd service. +```bash +podman run -d \ + --name fail2ban-ui \ + --network=host \ + -v /opt/podman-fail2ban-ui:/config:Z \ + -v /etc/fail2ban:/etc/fail2ban:Z \ + -v /var/log:/var/log:ro \ + -v /var/run/fail2ban:/var/run/fail2ban \ + -v /usr/share/GeoIP:/usr/share/GeoIP:ro \ + localhost/fail2ban-ui +``` + +### Stop and Remove Container +Stop the running container: +```bash +podman stop fail2ban-ui +``` +Remove the container: +```bash +podman rm fail2ban-ui +``` + +## Troubleshooting + +### UI Not Accessible +- Ensure port **8080 (or custom port)** is **not blocked** by the firewall. (e.g. firewalld) +- Check container logs: +```bash +podman logs fail2ban-ui +``` +- Ensure **Fail2Ban UI is running** inside the container: +```bash +podman exec -it fail2ban-ui ps aux +``` + +## Contact & Support +For issues, contributions, or feature requests, visit our GitHub repository: +πŸ”— [GitHub Issues](https://github.com/swissmakers/fail2ban-ui/issues) + +For enterprise support, visit: +πŸ”— [Swissmakers GmbH](https://swissmakers.ch) diff --git a/deployment/container/SELinux/fail2ban-container-client.mod b/deployment/container/SELinux/fail2ban-container-client.mod new file mode 100644 index 0000000..d9a5707 Binary files /dev/null and b/deployment/container/SELinux/fail2ban-container-client.mod differ diff --git a/deployment/container/SELinux/fail2ban-container-client.pp b/deployment/container/SELinux/fail2ban-container-client.pp new file mode 100644 index 0000000..dcdba3a Binary files /dev/null and b/deployment/container/SELinux/fail2ban-container-client.pp differ diff --git a/deployment/container/SELinux/fail2ban-container-client.te b/deployment/container/SELinux/fail2ban-container-client.te new file mode 100644 index 0000000..4247c88 --- /dev/null +++ b/deployment/container/SELinux/fail2ban-container-client.te @@ -0,0 +1,29 @@ + +module fail2ban-container-client 1.0; + +require { + type fail2ban_t; + type fail2ban_client_t; + type fail2ban_var_run_t; + type container_file_t; + type httpd_log_t; + type container_t; + type var_log_t; + class sock_file write; + class unix_stream_socket connectto; + class dir { read search open }; + class file { read open getattr }; +} + +#============= container_t ============== +allow container_t fail2ban_t:unix_stream_socket connectto; +allow container_t fail2ban_var_run_t:sock_file write; +allow container_t httpd_log_t:dir { read search open }; +allow container_t httpd_log_t:file { read open getattr }; +allow container_t var_log_t:dir { read search open }; +allow container_t var_log_t:file { read open getattr }; + +#============= fail2ban_client_t ============== +allow fail2ban_client_t container_file_t:dir { read search open }; +allow fail2ban_client_t container_file_t:file { read open getattr }; +allow fail2ban_client_t container_file_t:sock_file write; diff --git a/deployment/container/SELinux/fail2ban-container-ui.pp b/deployment/container/SELinux/fail2ban-container-ui.pp new file mode 100644 index 0000000..f102b53 Binary files /dev/null and b/deployment/container/SELinux/fail2ban-container-ui.pp differ diff --git a/deployment/container/SELinux/fail2ban-container-ui.te b/deployment/container/SELinux/fail2ban-container-ui.te new file mode 100644 index 0000000..78f663f --- /dev/null +++ b/deployment/container/SELinux/fail2ban-container-ui.te @@ -0,0 +1,13 @@ + +module fail2ban-container-ui 1.0; + +require { + type fail2ban_log_t; + type etc_t; + type container_t; + class file { open read write }; +} + +#============= container_t ============== +allow container_t etc_t:file write; +allow container_t fail2ban_log_t:file { open read }; diff --git a/internal/fail2ban-curl-allow.pp b/deployment/fail2ban-curl-allow.pp similarity index 100% rename from internal/fail2ban-curl-allow.pp rename to deployment/fail2ban-curl-allow.pp diff --git a/internal/fail2ban-curl-allow.te b/deployment/fail2ban-curl-allow.te similarity index 100% rename from internal/fail2ban-curl-allow.te rename to deployment/fail2ban-curl-allow.te diff --git a/deployment/systemd/README.md b/deployment/systemd/README.md new file mode 100644 index 0000000..ae252ac --- /dev/null +++ b/deployment/systemd/README.md @@ -0,0 +1,188 @@ +# Fail2Ban-UI Systemd Setup +This guide provides two methods to **run Fail2Ban-UI as a systemd service**. +1. Systemd service that starts the local compiled binary. +2. Systemd service that starts the fail2ban-ui container. + +## For SELinux enabled systems (needed in bouth cases) +If SELinux is enabled, you must apply the required SELinux policies to allow Fail2Ban to communicate with the Fail2Ban-UI API via port 8080. + +Apply the prebuilt SELinux Module with: + +```bash +semodule -i fail2ban-curl-allow.pp +``` + +## Build and running Fail2Ban-UI from Local Source Code +In this case we will run **Fail2Ban-UI from `/opt/fail2ban-ui/`** using systemd. + +### Prerequisites +Install **Go 1.22+** and required dependencies: + ```bash + sudo dnf install -y golang git whois + ``` +Make sure you setup GeoIP and your country database is available under: `/usr/share/GeoIP/GeoLite2-Country.mmdb` + +Clone the repository to `/opt/fail2ban-ui`: + ```bash + sudo git clone https://github.com/swissmakers/fail2ban-ui.git /opt/fail2ban-ui + cd /opt/fail2ban-ui + sudo go build -o fail2ban-ui ./cmd/main.go + ``` + +### Create the fail2ban-ui.service +Save this file as `/etc/systemd/system/fail2ban-ui.service`: + +```ini +[Unit] +Description=Fail2Ban UI +After=network.target fail2ban.service +Requires=fail2ban.service + +[Service] +WorkingDirectory=/opt/fail2ban-ui +ExecStart=/opt/fail2ban-ui/fail2ban-ui +Restart=always +User=root +Group=root + +[Install] +WantedBy=multi-user.target +``` + +### Start & Enable the Service +1. Reload systemd to detect our new service: + ```bash + sudo systemctl daemon-reload + ``` +2. Enable and start the service: + ```bash + sudo systemctl enable fail2ban-ui.service --now + ``` +3. Check the status: + ```bash + sudo systemctl status fail2ban-ui.service + ``` + +### View Logs +To see the real-time logs of Fail2Ban-UI: +```bash +sudo journalctl -u fail2ban-ui.service -f +``` + +### Restart or Stop +Restart: +```bash +sudo systemctl restart fail2ban-ui.service +``` +Stop: +```bash +sudo systemctl stop fail2ban-ui.service +``` + +## Running Fail2Ban-UI as a (Systemd controlled) Container + +This method runs Fail2Ban-UI as a **containerized service** with **automatic startup** and handling through systemd. + +### Prerequisites + +- Ensure **Podman** or **Docker** is installed. + +For **Podman**: +```bash +sudo dnf install -y podman +``` +For **Docker** (if preferred): +```bash +sudo dnf install -y docker +sudo systemctl enable --now docker +``` +Make sure you setup GeoIP and your country database is available under: `/usr/share/GeoIP/GeoLite2-Country.mmdb` + +Create the needed folder to store the fail2ban-ui config: +```bash +sudo mkdir /opt/podman-fail2ban-ui +``` + +### Create the fail2ban-ui-container.service +Save this file as `/etc/systemd/system/fail2ban-ui-container.service`: + +```ini +[Unit] +Description=Fail2Ban UI (Containerized) +After=network.target fail2ban.service +Requires=fail2ban.service + +[Service] +ExecStart=/usr/bin/podman run --rm \ + --name fail2ban-ui \ + --network=host \ + -v /opt/podman-fail2ban-ui:/config:Z \ + -v /etc/fail2ban:/etc/fail2ban:Z \ + -v /var/log:/var/log:ro \ + -v /var/run/fail2ban:/var/run/fail2ban \ + -v /usr/share/GeoIP:/usr/share/GeoIP:ro \ + localhost/fail2ban-ui +Restart=always +RestartSec=10s + +[Install] +WantedBy=multi-user.target +``` + +### For SELinux enabled systems +If SELinux is enabled, you must apply the required SELinux policies to allow the container to communicate with Fail2Ban. +The policies are located here: "`../container/SELinux/`" + +Apply the prebuilt SELinux Modules with: + +```bash +semodule -i fail2ban-container-ui.pp +semodule -i fail2ban-container-client.pp +``` + +#### Manually Compile and Install SELinux Rules + +If you want to change or compile the SELinux rules by yourself run: + +```bash +checkmodule -M -m -o fail2ban-container-client.mod fail2ban-container-client.te +semodule_package -o fail2ban-container-client.pp -m fail2ban-container-client.mod +semodule -i fail2ban-container-client.pp +``` + + +### Start & Enable the Container Service +1. Reload systemd to detect the new service: + ```bash + sudo systemctl daemon-reload + ``` +2. Enable and start the containerized service: + ```bash + sudo systemctl enable --now fail2ban-ui-container.service + ``` +3. Check the status: + ```bash + sudo systemctl status fail2ban-ui-container.service + ``` + +### View Logs +```bash +sudo journalctl -u fail2ban-ui-container.service -f +``` + +### Restart or Stop +Restart: +```bash +sudo systemctl restart fail2ban-ui-container.service +``` +Stop: +```bash +sudo systemctl stop fail2ban-ui-container.service +``` + +## **Contact & Support** +For issues, visit our GitHub repository: +πŸ”— [GitHub Issues](https://github.com/swissmakers/fail2ban-ui/issues) + +For enterprise support: +πŸ”— [Swissmakers GmbH](https://swissmakers.ch) diff --git a/screenshots/0_Dashboard.jpg b/screenshots/0_Dashboard.jpg new file mode 100644 index 0000000..ce6bfbc Binary files /dev/null and b/screenshots/0_Dashboard.jpg differ diff --git a/screenshots/0_dashboard.jpg b/screenshots/0_dashboard.jpg deleted file mode 100644 index 5f82bea..0000000 Binary files a/screenshots/0_dashboard.jpg and /dev/null differ diff --git a/screenshots/1_Dashboard_search.jpg b/screenshots/1_Dashboard_search.jpg new file mode 100644 index 0000000..c02d63d Binary files /dev/null and b/screenshots/1_Dashboard_search.jpg differ diff --git a/screenshots/1_dashboard_ bottom.jpg b/screenshots/1_dashboard_ bottom.jpg deleted file mode 100644 index e4533e0..0000000 Binary files a/screenshots/1_dashboard_ bottom.jpg and /dev/null differ diff --git a/screenshots/2_Dashboard_unban_IP.jpg b/screenshots/2_Dashboard_unban_IP.jpg new file mode 100644 index 0000000..66f6554 Binary files /dev/null and b/screenshots/2_Dashboard_unban_IP.jpg differ diff --git a/screenshots/2_edit_filter.jpg b/screenshots/2_edit_filter.jpg deleted file mode 100644 index 99cf0a6..0000000 Binary files a/screenshots/2_edit_filter.jpg and /dev/null differ diff --git a/screenshots/3_Dashboard_edit_filter.jpg b/screenshots/3_Dashboard_edit_filter.jpg new file mode 100644 index 0000000..72f8e3c Binary files /dev/null and b/screenshots/3_Dashboard_edit_filter.jpg differ diff --git a/screenshots/4_Dashboard_Reload.jpg b/screenshots/4_Dashboard_Reload.jpg new file mode 100644 index 0000000..c519840 Binary files /dev/null and b/screenshots/4_Dashboard_Reload.jpg differ diff --git a/screenshots/5_Filter_Debug.jpg b/screenshots/5_Filter_Debug.jpg new file mode 100644 index 0000000..0749d65 Binary files /dev/null and b/screenshots/5_Filter_Debug.jpg differ diff --git a/screenshots/6_Settings.jpg b/screenshots/6_Settings.jpg new file mode 100644 index 0000000..63048c0 Binary files /dev/null and b/screenshots/6_Settings.jpg differ diff --git a/screenshots/7_Settings_Bottom.jpg b/screenshots/7_Settings_Bottom.jpg new file mode 100644 index 0000000..53309c2 Binary files /dev/null and b/screenshots/7_Settings_Bottom.jpg differ diff --git a/screenshots/Example_block_email.pdf b/screenshots/Example_block_email.pdf new file mode 100644 index 0000000..3e844a7 Binary files /dev/null and b/screenshots/Example_block_email.pdf differ