Small web app to start, stop or recycle IIS Application Pools from a UI that works fine on a phone. Handy when an old site needs a quick kick and you're not in front of a desk
Runs on the same Windows Server / IIS that hosts the pools. Usually accessed through a VPN. If not, make sure you protect it some other way (firewall, IP allowlist, reverse proxy auth).
The scope is intentionally narrow:
| Allowed | Refused (out of scope) |
|---|---|
| List Application Pools (whitelisted) | Sites, bindings, virtual directories |
| Start / Stop / Recycle a pool | Creating or deleting pools / sites |
| Audit log (read only) | Editing files, web shell, server reboot |
Tech stack: ASP.NET Core 10 (LTS), Razor Pages, EF Core + SQLite, cookie auth,
Microsoft.Web.Administration, Bootstrap 5.
Prerequisites on the build host:
- .NET 10 SDK
- Windows with IIS (or IIS Management Tools) installed, provides
Microsoft.Web.Administration.dll
dotnet restore
dotnet publish -c Release -r win-x64 --self-contained false -o .\publishpublish/ is what you copy to the server.
-
Install the .NET 10 Hosting Bundle on the server (it ships the
AspNetCoreModuleV2IIS module). -
Create a folder, e.g.
C:\inetpub\IISWeb, and copy thepublish/content into it. -
Create a dedicated Application Pool, e.g.
IISWeb-AppPool:- .NET CLR version: No Managed Code
- Pipeline: Integrated
- Identity: see README-SECURITY.md for the recommended account and minimum privileges.
-
Create a Site or Application bound to that pool:
- Physical path:
C:\inetpub\IISWeb - Bindings: HTTPS only (port 443) with a valid certificate. HTTP can also be bound to issue a 301 redirect.
- Hostname: an internal-only DNS name reachable through the VPN only.
- Physical path:
-
Grant modify rights on the
App_Data\subfolder to the App Pool identity so it can create/update the SQLite file:icacls "C:\inetpub\IISWeb\App_Data" /grant "IIS AppPool\IISWeb-AppPool:(OI)(CI)M"
(Replace the App Pool name if you used a different one, or use the actual service account name.)
-
Edit
appsettings.Production.json— seeappsettings.Example.jsonfor the shape — and set:App.AllowedAppPools(whitelist)App.RequireHttps = true- any other tuning
Two equivalent options.
Run from a Windows shell, on the server, logged in as an Administrator:
cd C:\inetpub\IISWeb
.\IISWeb.exe seed-admin --username alice
# password is asked interactively (not echoed)Password must be at least 12 characters. Re-running the command for the same username fails.
If the database has no users when the app starts, IISWeb reads:
IISWEB_INITIAL_ADMIN_USERIISWEB_INITIAL_ADMIN_PASS
…and creates that admin once. Set these on the App Pool (Advanced Settings →
Environment Variables on IIS 10+, or web.config/environmentVariables),
then remove them after the first successful start.
| Key | Purpose |
|---|---|
AllowedAppPools |
Whitelist of pool names the app may see and act on. Empty array = all pools (not recommended in prod). |
AllowedIpRanges |
IP allow-list (single IPs or CIDR). Empty array disables filtering. See §6 below. |
LoginMaxAttempts |
Failed logins per account before lockout. |
LoginLockoutMinutes |
Lockout duration in minutes. |
RequireHttps |
Forces HTTPS redirection and Secure cookies. Set false only for local HTTP dev. |
SqlitePath |
Path to the SQLite file (relative paths are relative to the content root). |
SessionTimeoutMinutes |
Auth cookie sliding expiration in minutes. |
A separate global rate-limit of 10 login POSTs per minute per IP is applied at the framework level.
App.AllowedIpRanges is checked before authentication. Any request whose
remote IP is not in the list receives 403 Forbidden with no body. Even the
login page is invisible. Accepts both bare IPs and CIDR:
"AllowedIpRanges": [
"127.0.0.1", // loopback
"::1", // IPv6 loopback
"10.20.30.0/24", // VPN client subnet
"10.20.31.40" // a single jump host
]- An empty array disables the filter (default).
- Invalid entries are logged and ignored.
- If all entries are invalid the middleware fails closed (denies everything).
- The check runs after
UseForwardedHeaders, so when the app is behind a trusted proxy / IIS the original client IP is used. If you place an external reverse proxy in front, make sure to whitelist its rewriting behaviour and the proxy's network in the trusted-proxies section ofProgram.cs.
This is complementary to the VPN, not a replacement: it lets you scope the service to a specific admin subnet inside an already-private network.
- Navigate to the HTTPS hostname over the VPN.
- Sign in.
- The home page lists the whitelisted pools. Each card shows the current state and (for Admin users) Start / Stop / Recycle buttons. Buttons disabled when irrelevant for the current state.
- Every action is auditable in Audit log.
- Admin: sees pools and can Start / Stop / Recycle.
- Viewer: sees pools but cannot act. The data model already supports the
role; only Admin is created out of the box. Add a role check in
Pages/Index.cshtml.csto extend the matrix later.
IISWeb/
├── IISWeb.csproj
├── Program.cs # pipeline, auth, antiforgery, rate limit, CSP
├── CommandLine.cs # `seed-admin` CLI handler
├── appsettings*.json # configuration (Example, Development)
├── web.config # IIS / ANCM hosting config
├── Configuration/AppOptions.cs
├── Models/ # AppUser, Roles, AuditLog, AuditActions
├── Data/ # AppDbContext, DbInitializer
├── Services/ # IUserService, IAuditService, IIisPoolService
├── Pages/
│ ├── Account/Login + Logout + AccessDenied
│ ├── Index (pools)
│ ├── Audit
│ ├── Error
│ └── Shared/_Layout.cshtml
└── wwwroot/ # css / js (Bootstrap from CDN)
$env:ASPNETCORE_ENVIRONMENT = "Development"
dotnet run
# in another shell, seed an admin:
dotnet run -- seed-admin --username admin --password ChooseAStrongPwd!appsettings.Development.json disables HTTPS so the app works on plain HTTP
during development. Never ship RequireHttps=false to production.
{ "App": { "AllowedAppPools": ["DefaultAppPool", "MyApp"], "AllowedIpRanges": ["10.0.0.0/8", "192.168.0.0/16", "127.0.0.1", "::1"], "LoginMaxAttempts": 5, "LoginLockoutMinutes": 15, "RequireHttps": true, "SqlitePath": "App_Data/iisweb.db", "SessionTimeoutMinutes": 60 } }