# Apache Attack Mitigation — WordPress Probes and DokuWiki Crawl

**Host:** Devaun Daedalus (Debian, SysV init — *not* systemd), Apache 2, managed by ISPConfig3, fail2ban installed.
**Date of analysis:** 2026-09-13
**Status:** Configs written and regex-tested against captured log samples. **Nothing has been deployed.** No SVN add/commit performed. Apache-side rules are untested against the live server (see [Unverified Assumptions](#unverified-assumptions)).

This document supersedes the ad-hoc notes copied out of the chat session. It covers two separate incidents that arrived together but are unrelated, plus one configuration question they surfaced.

---

## 1. Environment and Log Format

All vhost traffic is logged to `/var/log/apache2/other_vhosts_access.log`, in addition to the per-site logs ISPConfig maintains. The format is a **site-local custom format under the nickname `fail2ban`** — not Apache's stock `vhost_combined`, which places `%t` fifth rather than first. It is defined in `/etc/apache2/apache2.conf` and consumed only by `/etc/apache2/conf-available/other-vhosts-access-log.conf`, so it can be changed without affecting anything else:

```
[13/Sep/2026:02:48:46 -0500] wolfstream.com:443 62.60.130.27 - - "GET /wp-admin/index.php HTTP/1.1" 403 3239 "https://wolfstream.com/wp-login.php" "Mozilla/5.0 ..."
|<---------------- %t ---------------->| |<-- %v:%p -->| |<--- %h --->| %l %u |<------------- "%r" ------------>| %>s  %O  |<------------ Referer ----------->| |<-- UA -->|
```

Corresponding directive:

```apache
LogFormat "%t %v:%p %h %l %u \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" fail2ban
```

See `apache-logformat.md` for the recommended replacement, which appends `\"%{Host}i\"` and `%D`. Both filters below work unchanged against it.

Field offsets for `awk`, confirmed against the samples: `$1`/`$2` are the split timestamp, `$3` is `vhost:port`, `$4` is the client IP.

### The one fail2ban behaviour that matters most here

**fail2ban strips the timestamp it matched out of the line before applying `failregex`.** A `failregex` that begins `^\[[^\]]+\]` therefore can never match this log — by the time the regex runs, the line no longer starts with a bracketed date. This single fact explains why the previously attempted filter caught nothing, and both filters below are built around it by declaring an explicit `datepattern` that consumes the whole bracketed timestamp, so the line handed to `failregex` reliably begins at the vhost field.

---

## 2. Incident A — WordPress Probe / Brute-Force Scanning

### Evidence

Sample: `apache.log` (16 lines). Two source IPs — `62.60.130.27` (4 hits) and `136.144.35.94` (12 hits) — requesting `/wp-admin/index.php`, `profile.php`, `edit.php`, `plugins.php` in rapid bursts, all answered `403` or `404`. User-Agent rotated across Chrome 118/119/121 and Windows/Mac. Users rarely log into wp-admin legitimately, so volume at this level indicates an attack in progress.

This is the conventional, easy case: few IPs, many hits each, error responses. Per-IP rate limiting works well.

### Why the previously attempted filter failed

Two independent bugs, both confirmed by running it:

1. **`failregex` anchored on the timestamp.** As described above, `^\[[^\]]+\]` cannot match. Testing showed fail2ban auto-selecting the date template `{^LN-BEG}Day/MON/ExYear...` and then missing all 16 lines.
2. **Filter name did not match the filename.** The jail said `filter = wp-admin-scan` but the filter file was `apache-wp-admin.conf`. The `filter` value must equal the filename minus `.conf`, so the filter would have failed to load even with a correct regex.

### Solution

| File | Install to |
|---|---|
| `apache-wp-probe.conf` | `/etc/fail2ban/filter.d/apache-wp-probe.conf` |
| `wp-probe.local` | `/etc/fail2ban/jail.d/wp-probe.local` |

Two design decisions in the filter, both to prevent false positives:

- **Only 4xx/5xx responses count as a failure.** A real administrator logging in gets 200/302 and is never counted. An anonymous visitor hitting `/wp-admin/` gets a 302 redirect and is never counted. This means `ignoreip` is not required to protect your own access.
- **`wp-content/` and `wp-includes/` only count when the request targets a `.php` file.** Counting any 404 under those paths would ban ordinary visitors of a real WordPress site whose theme references a missing image or stylesheet. A direct request for a `.php` file there is the plugin/theme vulnerability-scan pattern; legitimate assets are CSS, JS and images.

### Test results

| Test set | Expected | Result |
|---|---|---|
| `apache.log` (the real attack) | 16 / 16 matched | 16 ✅ |
| Extra probe patterns: `xmlrpc.php`, `wp-config.php.bak`, revslider `db.php`, `wlwmanifest.php`, `//wp-login.php?x`, `HEAD`/HTTP1.0, 500 | 7 / 7 matched | 7 ✅ |
| Legitimate traffic: 200 login POST, 302 `wp-admin`, 404 on `.png` and `.css`, 401 `wp-json` | 0 matched | 0 ✅ |

One negative test is worth remembering: a forged log entry was planted inside a User-Agent string (`"9.9.9.9 - - \"GET /wp-login.php HTTP/1.1\" 404 1"`). It correctly does **not** match. The anchoring is what prevents an attacker from getting a third party banned by stuffing fake log text into a request header — do not loosen it to an unanchored search.

---

## 3. Incident B — DokuWiki Media-Manager Crawl

### These are not failures

Sample: `unixservertech.log` (10 lines) against `kb.unixservertech.com`. The initial assumption was that these were failures. They are not — **all 10 returned HTTP 200.** DokuWiki served every request successfully.

The response sizes prove real content was rendered, not an error page:

| `tab_files` | Response sizes |
|---|---|
| `files` | 4961, 4984, 5141, 5905, 5940 |
| `upload` | 4706, 4710, 4722, 4722, 4727 |

`tab_files=upload` returns a near-constant ~4.7 KB — the upload form. `tab_files=files` varies with the `ns` parameter because it is listing the *actual files* in each namespace. A permission-denied page would not vary by namespace. Anonymous clients are successfully enumerating the media manager.

**Consequence:** this is resource consumption via scraping, not failed authentication, and it **cannot be detected by status code** in its current form.

### What it is doing

Brute-forcing the cartesian product of DokuWiki's media-manager parameters on a single page and a single image:

- `tab_details` ∈ {`view`, `history`} — 2 values
- `tab_files` ∈ {`files`, `upload`} — 2 values
- `ns` ∈ {`software`, `software/rust`, `software/dailydata`, `wiki`} — 4 values
- `image` — fixed at `software:rust:rustdeskdownload.png`

That is 16 combinations, 10 of which appear in the sample, each a full PHP page render. `do=media` is DokuWiki's fullscreen media manager: an editor UI surface whose tab/namespace parameters multiply combinatorially, which is exactly what makes it an attractive crawler trap.

### Why this is a bot

- All 9 source IPs are in Contabo / Netcup / Scaleway hosting ranges, not consumer ISPs
- Chrome User-Agents rotating across versions 122–125 and Windows/Mac, roughly one per request
- Combinatorial parameter enumeration, as above
- 8 of 9 IPs appear exactly once, all within 4 seconds

**Correction worth recording:** the constant cross-domain referer (`https://wiki.linuxservertech.com/`) was initially cited as a forged-referer bot signal. That was wrong — it is a legitimate legacy alias of this very site (see [Section 4](#4-the-legacy-domain-question)). The bot conclusion rests on the four points above and does not depend on the referer. The corrected reading is in fact more informative: a fixed bare-root referer across every hop, while requesting deep permuted URLs, is what a crawler does when **seeded with the old domain name** from a stale link or index and not updating the referer per hop.

### Why fail2ban alone is not sufficient

| | Incident A (WordPress) | Incident B (DokuWiki) |
|---|---|---|
| Requests | 16 | 10 |
| Distinct source IPs | 2 | **9** |
| Max hits from one IP | 12 | **2** |
| Response codes | 403 / 404 | **200** |

Eight of nine IPs appear exactly once. **No per-IP threshold can catch a swarm shaped like this** — the original `maxretry = 5` would have banned nobody. This is a distributed proxy pool; only `185.2.100.0/22` contributed more than one address.

### Solution — order of deployment is critical

**Step 1 is the actual fix. The fail2ban jail is only hygiene on top of it.**

| Step | File | Install to |
|---|---|---|
| 1 | `dokuwiki-block.md` | Top of DokuWiki's root `.htaccess` (recommended while tuning), or ISPConfig: **Sites → kb.unixservertech.com → Options tab → "Apache Directives"** |
| 2 | `apache-dokuwiki-crawl.conf` | `/etc/fail2ban/filter.d/apache-dokuwiki-crawl.conf` |
| 2 | `dokuwiki-crawl.local` | `/etc/fail2ban/jail.d/dokuwiki-crawl.local` |

**Step 1 — Apache returns 403 for `do=media` when the client sends no `Cookie` header at all.** Because Apache answers before PHP starts, each hit becomes a few hundred bytes of static response instead of a DokuWiki page render. This solves the resource-exhaustion problem by itself, regardless of how many IPs the attacker rotates through. Anonymous readers keep a fully working wiki: image fetches (`/lib/exe/fetch.php`), the single-image detail view, page views, search, diffs and exports are all deliberately untouched.

**Be clear about what this rule is: a cheap crawler filter, not an authentication gate.** Two facts rule out a genuine cookie-based auth check at the Apache layer. First, DokuWiki's session cookie name comes from the `DOKU_SESSION_NAME` constant — `inc/init.php` line 266 calls `session_name(DOKU_SESSION_NAME)` — which can be overridden in `inc/preload.php`, so a rule keyed on a literal name would break silently on upgrade. Second and more fundamentally, **DokuWiki starts a session for anonymous readers too**, so possessing a session cookie does not prove a user is logged in. No cookie test can distinguish an editor from an anonymous browser.

The cookieless test sidesteps both problems and works because of how this specific attack behaves: 9 IPs, one or two requests each, no prior page load, therefore no cookies. Any real browser that has loaded one wiki page already has a cookie and is unaffected. A crawler that accepts cookies and fetches a page first would get through — `dokuwiki-block.md` documents three stronger options (IP allowlist, scoped Basic auth, or a DokuWiki action plugin calling `auth_quickaclcheck()`) if that happens. Only the plugin route can make a real editor/reader decision, because Apache cannot see DokuWiki's auth state.

Also add `Disallow: /*?do=` to the wiki's `robots.txt`. It does nothing against this swarm, which ignored robots.txt, but it keeps compliant crawlers out of the parameter space entirely.

DokuWiki ACLs cannot achieve this on their own — the media manager is viewable with plain read permission — which is why the block belongs at the web-server layer.

Do **not** edit the `.vhost` file directly: ISPConfig3 regenerates `/etc/apache2/sites-available/*.vhost` and will discard the change. Two placements work — `.htaccess` in the wiki's web root, or the ISPConfig panel's Apache Directives field.

**Use `.htaccess` while tuning.** It takes effect on save, with no panel round-trip, no vhost regeneration and no Apache reload, which matters because this rule cannot be tested in advance. A mistake there also breaks only this site, whereas a bad directive reaching vhost config can stop Apache from starting and take down every site on the host. The rule ports cleanly between contexts: `%{QUERY_STRING}` and `%{HTTP_COOKIE}` are context-independent, and `RewriteRule ^ - [F,L]` substitutes nothing, so it behaves identically per-directory.

Two conditions apply. First, **paste it at the top of DokuWiki's existing root `.htaccess`, above DokuWiki's own `RewriteRule`s.** The attack URLs are pretty-URL form, so `userewrite` is enabled and a root `.htaccess` is already rewriting to `doku.php?id=$1 [QSA,L]`; placing this block below those rules means the `[L]` may already have ended rule processing. Second, `AllowOverride` must include `FileInfo` — ISPConfig's template usually sets `AllowOverride All`, verified with `grep -rn "AllowOverride" /etc/apache2/sites-enabled/ | grep -i unixservertech`.

Once the `curl` checks pass, optionally move it to the ISPConfig panel: that avoids the per-request `.htaccess` stat walk on every request to the site, and is immune to a DokuWiki upgrade overwriting the web root. If it stays in `.htaccess`, record that fact so a future wiki upgrade does not silently drop it.

**Step 2 — the filter counts only 4xx**, which is why step 1 must come first. An earlier draft matched `do=media` on any status and was discarded during testing: with `maxretry` low enough to catch a 9-IP swarm, your own editors opening the media manager twice would have been firewalled for a week. Gating on the 403 makes a match unambiguous — editors get 200 and are never counted. A commented-out status-agnostic variant is included in the filter for stopgap use before step 1 is live, with the same warning attached.

The jail uses `maxretry = 2` with a deliberately long `findtime = 6h`, because these IPs return over hours rather than seconds; the long window matters more than a low retry count. Real search-engine crawlers (Googlebot, bingbot, DuckDuckBot, Applebot, YandexBot) are exempted via `ignoreregex` — they identify honestly and obey robots.txt.

### Test results

| Scenario | Expected | Result |
|---|---|---|
| Attack traffic as it will look after the 403 rule | 10 matched | 10 ✅ |
| Attack traffic as captured today (200s) | 0 matched | 0 ✅ |
| Logged-in editor using the media manager (3× 200) | 0 matched | 0 ✅ |
| Legitimate reader, image fetch, `do=diff`, Googlebot | 0 matched | 0 ✅ (Googlebot counted as *ignored*) |
| WordPress filter vs. DokuWiki log | 0 matched | 0 ✅ |
| DokuWiki filter vs. WordPress log | 0 matched | 0 ✅ |

The last two confirm the jails do not interfere; both read the same log file, which fail2ban handles without issue.

---

## 4. The Legacy Domain Question

`wiki.linuxservertech.com` is an alias for `kb.unixservertech.com` — the old name, forwarded here several years ago.

### The measurement blind spot

The `LogFormat` uses **`%v`, which is the canonical `ServerName` of the serving vhost — not the `Host` header the client sent.** Therefore:

- If `wiki.linuxservertech.com` is a `ServerAlias` on the kb vhost, every request to it is logged as `kb.unixservertech.com:443` and is **invisible** in `other_vhosts_access.log`.
- It is consequently **not possible to tell from the current logs** whether the old name is an alias or a true 301 redirect, nor how much traffic it still receives.

So the question "should I retire it?" currently has no data behind it. Determine the present configuration first:

```bash
# Is it an alias (200) or a redirect (301)?
curl -sI https://wiki.linuxservertech.com/ | head -5

# How is it actually configured?
grep -rn "linuxservertech" /etc/apache2/sites-enabled/
```

### Getting the measurement without changing any server's LogFormat

**Recommended: give `wiki.linuxservertech.com` its own ISPConfig site that 301-redirects to the canonical host.** This needs no `LogFormat` change anywhere, and it is the same action already recommended on its own merits:

- A separate vhost has its own `ServerName`, so `%v` logs it distinctly — the measurement falls out for free, in both `other_vhosts_access.log` and its own per-site ISPConfig log.
- It converts the alias into a proper 301, which fixes the duplicate-content problem described below.
- Nothing global changes; no other server or site is touched.

In ISPConfig: create a new website for `wiki.linuxservertech.com`, then set **Redirect tab → Redirect Type `R=301`, Redirect Path `https://kb.unixservertech.com/`**, and issue a Let's Encrypt certificate for it. **Remove the existing `ServerAlias` from the kb site first**, or the new site will collide with it.

### Simpler option: add `%{Host}i` to the shared log

**Superseded by a better finding (2026-09-13).** The `other_vhosts_access.log` `LogFormat` turned out to use a private nickname, `fail2ban`, defined in exactly one place and consumed in exactly one place:

```
/etc/apache2/apache2.conf:LogFormat "%t %v:%p %h %l %u \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" fail2ban
/etc/apache2/conf-available/other-vhosts-access-log.conf:CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log fail2ban
```

Because that nickname is used by nothing else, editing it changes only this one log and affects no other site or server. So the `%{Host}i` field can simply be added there, and the per-vhost workaround below is no longer necessary. See `apache-logformat.md` for the recommended replacement, the rationale, and the apply procedure.

### Per-vhost alternative, no longer needed

Retained for reference only. Attach a second, `Host`-aware log to the kb vhost alone, via ISPConfig's Apache Directives field for that site:

```apache
LogFormat "%t %v:%p %h %l %u \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" \"%{Host}i\"" kb_hostlog
CustomLog /var/log/ispconfig/httpd/kb.unixservertech.com/host_access.log kb_hostlog
```

`LogFormat` and `CustomLog` are both valid in virtual-host context, so this affects only this site. Two caveats: the target directory must exist and be writable by Apache, and ISPConfig's log rotation will not know about this extra file — add a `logrotate` entry or it grows unbounded.

**Whenever you change a `LogFormat`, append new fields at the END.** Both filters key off field order up to the status code and ignore anything trailing, so appending is safe; inserting a field mid-line will break them. This was verified by re-running both filters against append-only converted copies of the two captured samples: `apache-wp-probe` 16/16, `apache-dokuwiki-crawl` 10/10 on 403s and 0 on 200s, with the filter files unmodified, and with no regression against the old format.

### Why the duplicate-content point matters here

If the old name is a `ServerAlias`, every wiki URL exists under two hostnames, which doubles the crawlable URL space — and that multiplies against the DokuWiki media-manager parameter explosion from Incident B. Same combinatorial problem, twice over. A 301 has none of that: one canonical URL space, and crawlers consolidate onto it.

### Recommendation: keep the name, do not retire it

1. **Do not let the registration lapse.** If `linuxservertech.com` expires, anyone can register it and use your former brand for phishing against clients who still have it bookmarked. That risk is permanent, and the domain fee is the cheapest control available.
2. A 301 costs essentially nothing — no PHP, no wiki involvement, no certificate complexity if it is a SAN.
3. After several years, a 301 has already consolidated search ranking onto the new name, and whatever inbound links and bookmarks remain keep working.

**Do not retire it in reaction to this incident.** The crawler appears to have found the site via a stale index entry for the old name; removing the name stops that one seed but not the crawler class, and the Apache 403 already neutralises the actual damage regardless of which hostname the request arrives on. Let the 301 site run 30 days, then revisit with real numbers.

---

## 5. Deployment Order

1. **Incident A (independent, deploy any time):** copy `apache-wp-probe.conf` and `wp-probe.local` into place, restart fail2ban, check jail status.
2. **Incident B step 1:** install `dokuwiki-block.md` — paste it at the **top** of DokuWiki's root `.htaccess`, above DokuWiki's own `RewriteRule`s. Verify with the `curl` checks below, and confirm in a browser that the media manager still opens for a logged-in editor. Move it to ISPConfig's Apache Directives afterwards if you prefer.
3. **Incident B step 2:** only after step 1 verifies, install `apache-dokuwiki-crawl.conf` and `dokuwiki-crawl.local`, restart fail2ban.
4. **Section 4 (independent, no hurry):** split `wiki.linuxservertech.com` into its own 301 site.

```bash
# fail2ban install pattern (same for both jails)
sudo cp apache-wp-probe.conf /etc/fail2ban/filter.d/apache-wp-probe.conf
sudo cp wp-probe.local       /etc/fail2ban/jail.d/wp-probe.local
sudo service fail2ban restart          # SysV — not systemctl
sudo fail2ban-client status wp-probe
```

```bash
# Verify the DokuWiki block (run from a shell, NOT a logged-in browser)
# Cookieless media manager — must print 403
curl -s -o /dev/null -w '%{http_code}\n' \
  'https://kb.unixservertech.com/start?do=media&ns=wiki'

# Same request WITH any cookie — must print 200, proving editors pass through
curl -s -o /dev/null -w '%{http_code}\n' -b 'x=1' \
  'https://kb.unixservertech.com/start?do=media&ns=wiki'

# Image fetch must still work — must print 200
curl -s -o /dev/null -w '%{http_code}\n' \
  'https://kb.unixservertech.com/lib/exe/fetch.php?media=software:rust:rustdeskdownload.png'
```

Re-testing a filter against a captured log, as a normal user with no sudo:

```bash
fail2ban-regex /path/to/sample.log /absolute/path/to/filter.conf
```

Note the filter argument must be an **absolute path**; a bare filename is parsed as a regex string instead of a file.

---

## 6. Unverified Assumptions

Carry these forward — they are the parts that could still be wrong.

- **The Apache `do=media` rule has not been tested against the live server.** It no longer depends on any DokuWiki cookie name — an earlier draft matched `DokuWiki=` and `DW<md5>=` literally, and was replaced after `grep -rn "session_name" inc/init.php` showed line 266 using the overridable `DOKU_SESSION_NAME` constant rather than a literal. The rule now tests only for the *absence of any* `Cookie` header. See Section 3 for why that is a crawler filter rather than an auth gate, and `dokuwiki-block.md` for stronger options.
- **`bantime.increment` requires fail2ban ≥ 0.11.** Both jail files use it. Confirm with `fail2ban-client --version` and delete those three lines per jail on 0.9/0.10.
- **`banaction` is deliberately unset in both jails**, so they inherit whatever ISPConfig/Debian configured in `jail.local`'s `[DEFAULT]` and stay consistent with the existing firewall. If bans appear to have no effect, check what that default actually is.
- **Both jails are placed in `jail.d/*.local`,** not `jail.local`. ISPConfig3 owns `jail.local` and rewrites it on update; files in `jail.d/` are left alone, and the `.local` suffix is read last so nothing in ISPConfig's `[DEFAULT]` can silently override these values.
- **`backend = auto` is set explicitly in both jails.** This host is SysV — this must never become `backend = systemd`.
- **Per-IP banning remains weak for Incident B even after deployment.** If the crawl continues, blocking at `/22` (starting with `185.2.100.0/22`, which supplied 3 of the 9 IPs) is far more effective than fail2ban's per-`/32` bans. This was left out of the configs on purpose: it also blocks legitimate users and VPNs behind those hosting providers, and that trade-off is a judgment call.
- **The filters are specific to this log's `fail2ban` field order.** The per-vhost ISPConfig logs under `/var/log/ispconfig/httpd/*/access.log` use plain `combined` — no vhost field, timestamp mid-line — so do **not** add them to either jail's `logpath`. They would need a separate filter with a different `datepattern` and `failregex`.

---

## 7. File Inventory

This document now lives under `sysadmin_scripts/fail2ban/` (an SVN working copy — see [Section 8](#8-repository-location)), reorganized by artifact type so each file's destination matches its path here minus the leading directory. See `../README.md` for which files are ISPConfig-specific versus portable to any host.

| File | Role |
|---|---|
| `Documentation/ProblemDefinition.md` | Original problem statement |
| `Documentation/MitigationPlan.md` | This document |
| `testing/apache.log` | Evidence sample — Incident A, 16 lines |
| `testing/unixservertech.log` | Evidence sample — Incident B, 10 lines |
| `filter.d/apache-wp-probe.conf` | fail2ban filter — WordPress probes → `/etc/fail2ban/filter.d/` |
| `jail.d/wp-probe.local` | fail2ban jail — WordPress probes → `/etc/fail2ban/jail.d/` |
| `filter.d/apache-dokuwiki-crawl.conf` | fail2ban filter — DokuWiki crawl → `/etc/fail2ban/filter.d/` |
| `jail.d/dokuwiki-crawl.local` | fail2ban jail — DokuWiki crawl → `/etc/fail2ban/jail.d/` |
| `apache-notes/dokuwiki-block.md` | Apache rewrite rules — wiki root `.htaccess`, or vhost/panel directives |
| `apache-notes/apache-logformat.md` | Recommended `other_vhosts_access.log` LogFormat + apply procedure |
| `scripts/getFail2BanJailStats` | Perl script — current/total bans per jail, tab-separated |
| `action.d/` | Empty, reserved for future custom fail2ban actions |

All files carry explanatory header comments; this document is the overview, not a replacement for reading them at install time.

## 8. Repository Location

This tree lives at `~/scripts/sysadmin_scripts/fail2ban/`, inside the `sysadmin_scripts` SVN working copy (`svn.dailydata.net/svn/sysadmin_scripts/trunk`). It started as scratch work in `~/sysadmin/` during the initial incident response and was moved here once the DokuWiki incident made clear this would be an ongoing collection, not a one-off.

Per this project's standing SVN convention, the files were relocated with plain `mv`, not `svn mv` — they never existed in the repository, so this is new content, not a rename of tracked history. The directory is expected to show as untracked (`?`) in `svn status` until the work is finished and syntax-validated; that is normal for this repository, not an oversight (other subprojects in the same working copy — `Wordpress/testing`, `deployStatic/testing` — are untracked the same way mid-build). `svn add` is listed here for you to run once, at the end, rather than run automatically:

```bash
cd ~/scripts/sysadmin_scripts
svn add fail2ban
```
