Pentester Console by AWHADI
Version: v1.23.0 · Changelog
A self-hosted web dashboard for running common Kali Linux pentesting tools
against targets you are authorized to test. It's a docker-compose stack
with two containers:
kali— a Kali Linux image with thekali-linux-headlessmetapackage (a large slice of everything Kali ships) plusopenssh-server, reachable only from thewebcontainer over an internal Docker network. It also refreshes its own packages and vulnerability databases in the background on every start — see Keeping Kali up to date.web— a Node.js/Express backend + static frontend. It supports multiple accounts with roles (see User management), optional single sign-on via any OpenID Connect provider (see Single sign-on), and outgoing email via SMTP (see Email (SMTP)) — all configurable from an admin-only Settings page. It shows a dashboard of tools with input forms and runs the chosen command inside thekalicontainer over SSH, streaming output back to the browser live via WebSockets. A "Custom Command" tool lets you run any other binary installed in the image.
- 21 tools across 9 categories — Nmap, Ping, Traceroute, tcpdump, ARP Scan, Whois, dig, theHarvester, DNSRecon, Nikto, Gobuster, ffuf, sqlmap, WPScan, Hydra, John the Ripper, Hashcat, SSLScan, Enum4linux, SMBMap, SearchSploit — plus an Automated Vulnerability Scan wizard and a Custom Command escape hatch for anything else installed in the image. Every tool also has a raw-arguments field to type the exact command yourself instead of using the structured form. See Available tools.
- Multi-session dashboard — run several tools at once, each in its own panel; minimize any of them to the bottom dock and keep working. A search box in the top bar jumps straight to any tool by name. Sessions survive a page refresh (they keep running and reconnect), and the maximum number of concurrent sessions is admin-configurable.
- Accounts, roles, and SSO — local accounts with
admin/userroles, or sign in via any OpenID Connect provider (Google, Microsoft/Entra ID, Keycloak, Okta, ...), with auto-provisioning and optional group-to-admin mapping. - Fine-grained access control — disable whole tool categories or individual tools for non-admins, with per-user overrides on top, plus a centrally-enforced password policy.
- Optional AI-assisted analysis — send a completed run's output to your own configured OpenAI-compatible provider (or a local one, e.g. Ollama) for a plain-language summary that calls out anything security-relevant; off by default, admin-configured, and IPs/MAC addresses are redacted before anything leaves the server. See Settings page.
- Email-backed self-service — password reset, one-time-code sign-in, and self-registration with confirmation, all opt-in and gated on a verified SMTP connection.
- Four built-in themes — Dark, Light, Aurora Glass, and Matrix, set as a site-wide default and overridable per-user.
- Runs anywhere — a single
docker compose up, an interactive installer, subpath deployment behind a reverse proxy, a mobile-friendly layout, and a non-root, structured-logging-by-default container forweb. - Local-network access is opt-in — by default the
kalicontainer can only reach the public internet, not your LAN; the installer asks, and it can be changed anytime viaKALI_ALLOW_LOCAL_NETWORK. See Security notes.
This stack executes real offensive-security tools (port scanners, brute forcers, SQL injection testers, password crackers, etc.) against whatever target you point it at. Only use it against systems and networks you own or have explicit written authorization to test. Do not expose this site to the public internet — see Security notes below.
- Prerequisites
- Setup
- Using it
- Available tools
- User management
- Settings page
- Access control
- AI-assisted analysis
- Password policy
- Themes
- Single sign-on (OIDC)
- Email (SMTP)
- Keeping Kali up to date
- Backup and restore
- Adding more tools
- Choosing how much of Kali to install
- Security notes
- Troubleshooting
- Docker and Docker Compose v2 (
docker compose version) - ~10–20 GB free disk space (the
kali-linux-headlessmetapackage is large)
./installer.shThis interactive installer checks Docker (and Docker Compose) is available
and running (offering to start Docker Desktop on macOS), asks which port to
use and whether you're serving this from a subpath behind a reverse proxy,
lets you pick how much of Kali to install, asks whether kali should be
allowed to reach your local network (LAN) in addition to the public
internet (see Security notes), and optionally builds and
starts the stack. Re-run it anytime — it shows your current settings and
lets you update any of them instead of starting over.
Skip to Manual setup if you'd rather do each step yourself, or if you're troubleshooting something the wizard already ran.
-
Configure environment variables:
cp .env.example .env
Edit
.envand setSESSION_SECRETto a long random string, e.g.openssl rand -hex 32.WEB_PORTandKALI_METAhave sensible defaults — see Choosing how much of Kali to install. -
Start the stack:
docker compose up --build -d
The first build installs the Kali metapackage and can take a long time depending on your connection and disk speed. The
kalicontainer generates its own SSH keypair on first boot (shared withwebover a Docker volume) — nothing to set up for that yourself. -
Visit http://localhost:3000 (or whatever
WEB_PORTyou set). Since no accounts exist yet, you'll be asked to create the first admin account right there in the browser — see User management.
By default the app expects to be served from a domain's root
(https://example.com/). To serve it from a subpath instead
(https://example.com/pentester/), set BASE_PATH in .env (no trailing
slash, e.g. BASE_PATH=/pentester) — installer.sh asks for this too.
Every internal link, API call, and the WebSocket connection automatically
adjust to it, so the reverse proxy just needs to pass the subpath straight
through, with no path-rewriting:
location /pentester/ {
proxy_pass http://web:3000/pentester/;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
If you're also using SSO, set PUBLIC_URL to the full external URL
including the subpath (e.g. https://example.com/pentester) so the OIDC
redirect_uri matches what's registered with your identity provider — see
Single sign-on (OIDC).
- The dashboard groups tools by category (Reconnaissance, Web, Password Attacks, etc.). Click a tool card to open its input form, or use the search box in the top bar to jump straight to one by name.
- Every tool's form has a raw-arguments field at the bottom — fill in the structured fields above it as usual, or type the exact arguments yourself there instead (it overrides the fields above when used).
- Output streams live in the panel on the right; use Stop to kill a long-running scan.
- Run several tools at once — each opens its own panel. Minimize a panel to shrink it to a chip in the dock at the bottom of the screen (it keeps running and buffering output in the background); click the chip to bring it back, or Close all to tear down every open session at once.
- A green/red dot on each card shows whether that tool's binary is actually
present in the
kaliimage (useful if you swapKALI_METAfor a smaller metapackage). - Custom Command (under "Advanced") runs any binary installed in the Kali image with arbitrary arguments — use this for anything not covered by a dedicated form.
- Automated Vulnerability Scan (under "Automated Scan") is a wizard: give it a target, optionally a login (HTTP Basic/Digest, or a web form's login URL + field names + username/password), and it chains Nmap → Nikto → Gobuster → WPScan (and SQLMap, if you give it a URL with a parameter to test) against the target automatically, then ends with a report of what ran and whether each step found something. Form login works by POSTing your credentials to the login URL with curl and reusing the session cookie in later steps — it only handles simple POST forms (no CSRF token, no JavaScript-rendered login) and does not itself verify the login succeeded, so check the HTTP status in the "Log in" step's output. The report's summaries are automated heuristics (counting lines, matching keywords) — always check the raw output too. Click Download on the report to save it as a text file.
- Update Kali Tools (under "Maintenance", admin only) — see Keeping Kali up to date.
- Tools that operate on hash files or wordlists (John, Hashcat, Hydra)
expect the file to already exist inside the
kalicontainer. Kali's bundledrockyou.txtwordlist is auto-unzipped on first start. To add your own files,docker cp yourfile.txt pentester-kali:/root/or mount a volume indocker-compose.yml.
| Category | Tool | What it does |
|---|---|---|
| Reconnaissance | Nmap | Port scanning and service/version detection. |
| Reconnaissance | Ping | Check whether a host responds and measure round-trip latency. |
| Reconnaissance | Traceroute | Show the network path (hop by hop) to a host. |
| Reconnaissance | tcpdump | Live packet capture on a network interface. |
| Reconnaissance | ARP Scan | Discover live hosts via ARP — only on this container's own Docker network, not the wider LAN. |
| Reconnaissance | Whois | Domain/IP registration lookup. |
| Reconnaissance | DNS Lookup (dig) | Query DNS records for a domain. |
| Reconnaissance | theHarvester | OSINT: harvest emails, subdomains and names from public sources. |
| Reconnaissance | DNSRecon | DNS enumeration and zone transfer testing. |
| Web | Nikto | Web server vulnerability scanner. |
| Web | Gobuster (dir) | Directory/file brute-forcing on a web server. |
| Web | ffuf | Fast web fuzzer for directories, parameters and vhosts. |
| Web | sqlmap | Automated SQL injection detection and exploitation. |
| Web | WPScan | WordPress vulnerability scanner. |
| Password Attacks | Hydra | Online login brute-forcer for network services. |
| Password Attacks | John the Ripper | Offline password hash cracker. |
| Password Attacks | Hashcat | GPU/CPU-accelerated hash cracking. |
| SSL/TLS | SSLScan | Enumerate SSL/TLS ciphers and certificate info. |
| Windows/SMB | Enum4linux | Enumerate SMB shares, users and groups on Windows/Samba hosts. |
| Windows/SMB | SMBMap | Enumerate SMB share permissions. |
| Exploitation | SearchSploit | Search the offline Exploit-DB archive for known exploits. |
| Maintenance | Update Kali Tools (admin only) | Refresh installed packages and the Exploit-DB/Nikto/WPScan databases. |
| Automated Scan | Automated Vulnerability Scan | Wizard: optional login, then chains Nmap → Nikto → Gobuster → WPScan (+ SQLMap) with a report at the end. |
| Advanced | Custom Command | Run any binary installed in the Kali image with arbitrary arguments. |
See Adding more tools to add your own.
Accounts have a role: admin or user. Admins can manage other
accounts and see admin-only tools (currently just "Update Kali Tools");
everyone can run the rest of the dashboard and change their own password.
-
First run: since no accounts exist yet, opening the site shows a "create the first admin account" form instead of a login form — pick whatever username and password you want there. That account (or the first person to sign in via SSO, if you've set that up — see below) becomes the first admin.
-
Scripted/headless first admin: for automated deployments where nobody will click through the browser, set
INITIAL_ADMIN_USERNAMEandINITIAL_ADMIN_PASSWORD(orINITIAL_ADMIN_PASSWORD_FILE, pointing at a file/Docker secret) in.envbefore first start. If no accounts exist yet when thewebcontainer boots, it creates that admin automatically and logs it. This only ever fires once — once any account exists, these variables are ignored, so it's safe to leave them set. If your password contains a literal$, double it ($→$$) — see the Compose$-interpolation note further down; this is exactly why the_FILEvariant (reading from a mounted file, untouched by Compose) is the safer choice for automation. -
Adding/removing users: an admin opens the round avatar button in the top bar → Manage Users (or Settings → Users) and clicks + Add User, which opens a popup to fill in username, email, password, and role. From the Users table an admin can also edit anyone's email, reset anyone's password, promote/demote between
userandadmin, or delete an account. You can't delete or demote the last remaining admin, and you can't delete or change the role of the account you're currently logged in as (avoids accidental lockouts). -
Changing your own password/email/photo: any logged-in user can do this via My Account from the avatar menu (password change requires the current password; admins do this from their own row in Manage Users instead). The same dialog lets you upload a profile photo (shown in the avatar button instead of your initials) or remove it.
-
Password reset via email: the login page's Forgot password? link emails a reset link (valid 45 minutes, single-use) to the address on file for that account. This link only appears once an admin turns on Allow password reset via email in Settings → Email (SMTP) — turning it on re-verifies the configured SMTP server actually connects first, so it can't be enabled with a mail server that doesn't work. The response is always the same generic message regardless of whether the account/email exists, so the login page can't be used to enumerate accounts.
-
One-time-code sign-in: the login page's Use a one-time code instead link emails a 6-digit code (valid 10 minutes, single-use, locked out after 5 wrong attempts) instead of asking for a password — same SMTP-verified opt-in (Allow one-time-code sign-in via email in Settings → Email (SMTP)) and anti-enumeration behavior as password reset.
-
Optional public self-registration: disabled by default. An admin can turn it on in Settings → General ("Allow public self-registration"), which - like password reset and one-time codes - re-verifies SMTP first; when on, the login page shows a Register link. Self-registered accounts are always created with the
userrole - the request body's role, if any, is ignored server-side, not just hidden in the UI - and must confirm their email (a link valid for 24 hours, sent on registration) before they can sign in; the account doesn't exist in a usable form until then. Signing in beforehand fails with a clear error and a Resend confirmation email link. An admin can see who's still pending in Settings → Users (tagged "pending confirmation") and activate an account manually with Activate now, e.g. if the email never arrives. -
Single sign-on kill switch: Settings → General also has Enable single sign-on, on by default. It doesn't touch any configured provider - it just hides the "Sign in with ..." buttons and blocks the callback routes while off, so you can temporarily turn off SSO without deleting/reconfiguring providers.
-
Locked out / recovery: create or reset an account directly, without the browser:
docker compose exec web node create-admin.js <username> <password> [admin|user]
If the username exists, this resets its password (and role, if given); otherwise it creates a new account (admin, unless you specify
user).
Accounts (including email addresses) are stored in backend/data/users.json
inside a named Docker volume (web-data), so they survive
docker compose up --build. Sessions and pending password-reset/OTP
codes are in-memory only, so everyone is logged out and any
not-yet-used reset link or code is invalidated if the web container
restarts — that's unchanged from before and just means signing back in or
requesting a new code/link, not losing any accounts.
Everything admin-only lives on its own page at /settings.html, opened
from the round avatar button in the top bar (admins see extra links there;
non-admins just get My Account and Log out) — it has its own
sidebar with six sections: General Settings, Users,
Single Sign-On, Email (SMTP), AI Analysis, Analytics,
Privacy & Cookies (with the cookie scanner), and Access Control. Each
avatar-menu item deep-links straight to its section (e.g. Email (SMTP)
opens /settings.html#smtp). Non-admins are redirected away from the page if
they navigate there directly (the underlying API routes are admin-gated
too, so this isn't just a UI hint). Users and SSO providers are both
added through a popup (+ Add User / + Add Provider) rather than an
always-visible inline form, so the page stays uncluttered until you
actually want to add one.
Settings → Access Control lets an admin disable entire tool categories (e.g. "SSL/TLS", "Windows/SMB") or individual tools within an otherwise enabled category (e.g. keep "Web" on but turn off just "sqlmap") for everyone except admins. Disabling a whole category always wins even if a tool's own checkbox is on. Disabled categories/tools disappear from non-admins' dashboards and are rejected server-side if requested directly (e.g. by calling the WebSocket API without going through the UI), not just hidden client-side. Admins always see and can run everything regardless of this setting, and admin-only tools (like "Update Kali Tools") are unaffected — this is a separate, coarser control on top of that.
Per-user overrides: in Settings → Users, each non-admin account has a Manage Access button opening the same tool list, but with a three-way choice per tool — Default (follow the settings above), Always allow, or Always block. A per-user override always wins for that one tool, in either direction, over the general Access Control settings; a tool left on "Default" just falls back to them. This lets you, for example, keep a whole category off for everyone but make an exception for one person.
Settings → AI Analysis lets an admin connect an OpenAI-compatible endpoint (OpenAI itself, a local server like Ollama or LM Studio, or any other provider speaking the same chat-completions API) and turn on an Analyze button that appears once a tool run finishes. Sending a run's output for analysis is a per-run, explicit action — nothing is sent automatically. Off by default; requires both enabling it and a successful connection test. Before anything is sent, IP addresses and MAC addresses in the output are redacted. The prompt asks the model to specifically call out anything security-relevant (vulnerabilities, misconfigurations, credentials, interesting findings) rather than just restate the output. Available to every signed-in user once an admin has configured and enabled it — a non-admin never sees the provider's endpoint or API key.
Settings → Analytics lets an admin measure visits with Google Analytics 4, Plausible, Umami, Matomo or Cloudflare Web Analytics: pick the provider, enter its ID (and its URL for Plausible / Umami / Matomo), switch it on, and save. No code is pasted — the server adds the provider's script itself, escaped and with the page's CSP nonce, and widens that page's Content-Security-Policy by exactly the provider's own domains (shown on the page under What will be added to pages). Off by default; turning it off stops loading the script everywhere and keeps the saved IDs.
It always runs on the public pages (homepage, sign-in, legal pages, check-email). Also track signed-in pages (dashboard and My Account) is a separate, off-by-default switch. It never runs on the Settings pages, the email-confirmation and password-reset pages, or any page opened from a link carrying a one-time token / invitation (analytics services record the full page address). Respect Do Not Track and Global Privacy Control (on by default) skips visitors whose browser sends either signal. Update your Privacy Policy under Settings → Pages when you turn it on.
Settings → Privacy & Cookies makes the app work for the GDPR / ePrivacy rules in Europe — and the UK, Swiss, Brazilian, Quebec and US rules too — without any setup. The page is five cards, built like the other settings pages: a Compliance check (a "Ready" / "n to fix" pill, with a one-click fix for anything that isn't), the Cookie banner (always previewed, with its title and text), Who runs this site, Visitors' data and the Cookie scanner. What regulators fix is not a setting: the choices that used to be here (how long a choice is remembered, how long records are kept, the always- visible Cookie settings button, relaxing the rule outside Europe) keep their safe defaults. These are tools to help; they are not legal advice.
- One strict rule for everyone. Optional cookies (analytics) only run after the visitor says yes, wherever they are; Reject all, Choose and Accept all are on the first screen with identical styling; Do Not Track and Global Privacy Control are always honoured (the panel shows a "signal honoured" note). That is what the EU/EEA, UK, Switzerland, Brazil and Quebec require, and being stricter it also satisfies the US opt-out laws, so no country detection is needed. An install that already relaxed the rule for visitors outside Europe keeps working, and the Compliance check offers a one-click fix back to "ask everyone first".
- A cookie banner built from what the site really sets. A compact card in the bottom-left corner, in the app's own look (same corners, mono label, theme colours - it follows Spectrum, Midnight and Daylight - and a full-width sheet on phones). It never covers the page or locks its scrolling. It has a short explanation, one row per group — Functional (always active) and Statistics (a switch; Marketing appears only if anything is ever found in it) — each opening to list its cookies, provider, purpose and lifetime, and two equal buttons, Accept and Deny. Save my selection appears once a switch has been changed. The X before a first choice counts as Deny, and Escape counts only while the card has focus. Functional can't be switched off — the login cookie, the cookie that remembers the choice, bot protection, interface preferences, and the free-usage limit for visitors without an account (applied by IP address on the server, no cookie, so declining statistics never gives anyone more free runs). A small always-visible Cookie settings button (plus a footer link) reopens the card, so withdrawing is as easy as consenting.
- Trying it out. The preview on the page always shows the banner (with a sample Statistics entry while Analytics is off). Visitors only get it while Analytics is on; then See it on the site opens your homepage with the banner showing on every page load, for you alone while signed in as an admin — nothing you choose is recorded. Stop testing switches it off.
- Consent you can prove, and re-ask when things change. Every choice is recorded (time, categories, version, a random id — no IP address) for 36 months and can be downloaded as CSV. The stored choice is tied to the site's optional cookies: when the scan finds a new one, visitors are asked again. Choices are remembered for 6 months.
- Operator details and a complete Privacy Policy. Enter who is responsible (name and contact email are required; address, DPO, hosting country and governing law are optional) once; the policy fills its "[Operator contact email]"-style gaps and gains a generated section — who is responsible, every cookie, why data is used and the legal basis, how long each kind of data is kept (from the app's real limits), who else receives data, your rights and the right to complain. It stays accurate as you turn analytics, bot protection or the AI assistant on or off.
- Users' data rights. Signed-in users can download my data (a JSON file: account, sign-ins, activity, usage) and delete my account (password- confirmed; SSO-only accounts type their username) from My Account. Deleting removes the account, its sign-in history and its activity and usage records, and ends its sessions. The last administrator can't delete their account. An admin switch turns both off.
- Upgrading from an older version. The previous consent "modes" are gone; an existing setting is replaced by the strict rule (so an install only gets stricter, and the page says so), and every visitor sees the panel once more.
The cookie scanner is part of Settings → Privacy & Cookies. It requests
the public pages (homepage, sign-in, legal pages, check-email) as five visitors
— a new one from an EU country, one from the US, one who accepted analytics, one
who rejected it, and one sending Global Privacy Control — reads each response
(scripts, frames, images, the cookie panel, Set-Cookie headers) and looks every
cookie and outside host up in a built-in catalogue. About 30 requests, not
counted as visits. It runs by itself after the app starts and a moment after any
setting that affects cookies changes; Scan site now runs it on demand.
Its checks feed the status at the top of the page: nothing optional loads before anyone agrees, the panel is on every page with an equal Reject and a Cookie settings button, analytics stays off after a "no" and for Global Privacy Control, no cookie lasts longer than 13 months, no unknown cookies or third-party addresses, outside-EU transfers, and reCAPTCHA / Google Fonts warnings. The scan also supplies the cookie list the panel and the Privacy Policy show, plus tables of every cookie and storage item, every third-party service and what each visitor type receives, and a card listing the cookies this browser is sending right now (names only, including HttpOnly ones).
Limits: it can't see cookies a third-party script sets in a visitor's browser at run time, so those come from the catalogue for the services it detects; signed-in pages and Settings aren't scanned. Google Analytics cookies are set to last 13 months instead of Google's 2-year default.
Settings → General Settings has a password policy (minimum length,
and optionally requiring a number and/or a special character) that's
enforced centrally everywhere a password is set — self-registration,
password reset via email, admin-created accounts, admin password resets,
and a user changing their own password — so there's no path that bypasses
it. The registration and change-password forms show the current
requirements and set their minlength accordingly.
Four built-in themes — Dark, Light, Aurora Glass (a frosted,
translucent look, the default), and Matrix (a green-on-black terminal
look) — selectable in Settings → General Settings as the site-wide
default for accounts that haven't chosen one, and overridable by any user
for themselves in My Account. Applied via a data-theme attribute on
<html> (internal theme keys are dark/light/glass/hacker), so a
new theme just needs a CSS custom-property block in style.css — no
per-component overrides required except where a theme changes the overall
page treatment (see the glass/hacker blocks for examples).
The login page can offer "Sign in with ..." buttons for any OpenID Connect provider — Google, Microsoft/Entra ID, Keycloak, Okta, Authentik, etc. It's generic: the app talks standard OIDC discovery/authorization-code-with-PKCE, so anything spec-compliant works, not just the three below. It's entirely optional and additive — local username/password login keeps working alongside it.
How accounts work with SSO: the first person to sign in via SSO when no
accounts exist yet becomes the first admin, same as the browser bootstrap
form. After that, a new IdP identity signing in for the first time gets a
new local account (role user, unless mapped to admin — see the admin
group setting below), linked to that identity by its subject (sub)
claim, not just the username, so it keeps working if the display
name/email changes upstream. Admins can manage these accounts in
Settings → Users like any other, except there's no password to reset —
they can only sign in through that provider (shown as "via <provider>"
in the list).
Go to Settings → Single Sign-On → + Add Provider. Type a display
label (e.g. "Keycloak") and its sign-in key is generated for you
automatically as you type — edit it directly if you'd rather pick your
own. Fill in the issuer URL, client ID and secret, pick which ID-token
claim (preferred username, email, full name, or subject ID) becomes the
new account's local username under Login field from SSO, then look at
the highlighted Redirect URI box that appears: that's the exact URL to
register with your identity provider (see the per-provider notes below for
where). Click Copy, register it, save, and the provider immediately
appears as a "Sign in with ..." button on the login page. A provider added
here always takes priority over one configured via environment variables
with the same key, and editing later never requires touching .env or
restarting the container.
-
Pick a short key for the provider (e.g.
keycloak,google,microsoft, or any name you like) and add it toOIDC_PROVIDERSin.env(comma-separated if enabling more than one). -
Register this app with the IdP and set its redirect URI to:
<PUBLIC_URL or http://localhost:3000>/api/oidc/<key>/callbackSet
PUBLIC_URLin.envif you're behind a reverse proxy or a different host/port thanhttp://localhost:3000— it must match exactly what you register. -
Fill in
OIDC_<KEY>_ISSUER,OIDC_<KEY>_CLIENT_ID, andOIDC_<KEY>_CLIENT_SECRET(or_CLIENT_SECRET_FILE) in.env. See the commented examples in.env.examplefor the three providers below. Optionally setOIDC_<KEY>_USERNAME_CLAIMtopreferred_username(default),email,name, orsubto control which ID-token claim becomes the new account's username. -
docker compose up -d webto apply.
Google (console.cloud.google.com/apis/credentials):
create an OAuth 2.0 Client ID (type "Web application"), add the redirect
URI from step 2 under "Authorized redirect URIs". Issuer is always
https://accounts.google.com.
Microsoft / Entra ID (entra.microsoft.com →
App registrations): register a new app, add the redirect URI as a "Web"
platform redirect, and create a client secret under "Certificates &
secrets". Issuer is
https://login.microsoftonline.com/<tenant-id>/v2.0 (use your tenant ID
from the app's Overview page, or common to allow any Microsoft account).
Keycloak: in your realm, create a client with "Client authentication"
on (confidential), add the redirect URI from step 2 under "Valid redirect
URIs", and copy the client secret from the client's "Credentials" tab.
Issuer is https://<your-keycloak-host>/realms/<realm>. Set
OIDC_KEYCLOAK_ADMIN_GROUP=<realm-role-name> to automatically grant the
admin role to anyone with that realm role — it's re-checked on every
login, so revoking it in Keycloak takes effect the next time they sign in.
Settings → Email (SMTP) lets an admin configure an outgoing mail
server (host, port, TLS, credentials, from address/name) so the app can
send email. This powers password-reset links and one-time sign-in codes
(see User management), and the Send a Test Email
button sends a real message to an address you give it to confirm the
whole chain works before relying on it. As with SSO, a configuration
saved here always takes priority over the equivalent SMTP_*
environment variables (see .env.example),
and you never need to restart the container to change it. Leaving the
password field blank while editing keeps the currently saved password.
Stale tools and vulnerability databases mean missed findings, so this stack refreshes them in two ways:
- Automatically, in the background, every time the
kalicontainer starts: it runsapt-get update && apt-get upgrade, then refreshes SearchSploit's Exploit-DB copy, Nikto's plugin database, and WPScan's vulnerability data. This happens in the background so it never delays SSH/the dashboard becoming available. Progress and errors are logged to/var/log/kali-auto-update.loginside the container (docker compose exec kali cat /var/log/kali-auto-update.log). - On demand: an admin can click Update Kali Tools in the dashboard
to run the same refresh and watch it happen live, instead of waiting for
the next restart. It needs the
kalicontainer to have internet access and can take several minutes.
./backup.sh # writes to backups/<timestamp>/
./restore.sh backups/<timestamp>backup.sh dumps the Postgres database (accounts, settings, custom tools,
logs) and archives the web-data/ssh-keys volumes (legacy not-yet-migrated
JSON files, and the SSH keypair the web app uses to reach Kali) into a
timestamped directory under backups/ (gitignored — never committed). It
needs the stack already running (docker compose up -d).
It deliberately leaves out two things: .env (host-specific secrets —
SESSION_SECRET, POSTGRES_PASSWORD, API keys — never bundled into
something that might get copied elsewhere) and the kali-home volume (just
the Kali container's own OS/package state, not app data — rebuilt by
docker compose build). A restore target needs its own .env already in
place first.
restore.sh <backup-dir> is destructive — it overwrites the current
database and both volumes with the backup's contents, after a type RESTORE to confirm prompt (skip it non-interactively with --yes). It briefly
stops the web container during the restore (Postgres and Kali stay up)
and starts it back up when done.
Tool definitions live in backend/src/tools/definitions.js.
Each entry declares an id, name, category, description, the
binary to run, the input fields to render as a form, and a
buildArgs(params) function that validates user input and returns the
argument array (never build a raw shell string — always return an array so
the backend can safely quote it before sending it to Kali over SSH). Add
adminOnly: true to restrict a tool to admin accounts (enforced both when
listing tools and when a run is requested, not just hidden in the UI). The
automated scan wizard is the exception — it's a multi-step pipeline defined
in backend/src/pipeline.js instead of a single
buildArgs, since later steps (Gobuster, WPScan, SQLMap) need the session
cookie an earlier login step captures.
KALI_META in .env controls the metapackage installed in the kali
image (see kali/Dockerfile). Options, roughly smallest to largest:
kali-linux-core— minimal base.kali-linux-default— the standard desktop tool set.kali-linux-headless— default here; broad tool coverage, no GUI/desktop cruft, reasonable for a container.kali-linux-large/kali-linux-everything— essentially all Kali packages; multi-tens-of-GB and a long build. Only use if you have the disk space and need something obscure.
You can also docker compose exec kali apt-get install -y <package> to add
one specific tool without rebuilding.
- Accounts and roles. See User management above.
Sessions are signed with
SESSION_SECRETand held in memory (a singlewebprocess, not distributed) — rotateSESSION_SECRETto invalidate all sessions at once. - Don't expose this publicly. It runs privileged scanning/attack tools on demand. If you need remote access, put it behind a VPN or an authenticated reverse proxy with TLS, and restrict source IPs.
- Local-network (LAN) access is opt-in. By default
KALI_ALLOW_LOCAL_NETWORK=false, and thekalicontainer's entrypoint installsiptables/ip6tablesrules that reject outbound traffic to RFC1918/link-local/ULA address ranges — it can still reach the public internet, just not devices on your LAN — with an explicit exception for loopback and already-established connections (so theweb→kaliSSH control channel keeps working). Set it totrue(or answer yes ininstaller.sh) if you need to scan your own local network; on a shared or work network, leaving it off means a compromised or misused tool run can't reach anything beyond thekalicontainer itself. - AI-assisted analysis sends data to a third party, if you turn it on. See AI-assisted analysis — it's off by default. Once enabled, running an analysis sends that run's (redacted) output to whatever endpoint is configured — a provider you control the trust relationship with, including a fully local option like Ollama if you'd rather nothing leave the host at all.
- The
kalicontainer runs as root withNET_ADMIN/NET_RAWcapabilities so tools likenmap -sSwork. It only accepts SSH connections authenticated by a keypair it generates itself on first boot and shares withwebover thessh-keysDocker volume (never written to the host filesystem) — password auth is disabled inkali/sshd_config. Its own connection logging is intentionally quiet (LogLevel ERROR) since the app opens a fresh SSH connection per command — otherwise every tool run would add an "Accepted publickey" / "Disconnected" triplet todocker compose logs. - The
webcontainer runs as the unprivilegednodeuser, not root. If you're upgrading an existing deployment whoseweb-datavolume was created by an older (root) image, fix its ownership once after pulling the new image:docker compose exec -u root web chown -R node:node /app/data. A fresh deployment doesn't need this — the image creates the directory with the right owner itself. - Input handling. Every form field is validated against a strict
allowlist regex (host/IP, port range, path, safe token) before being
turned into a command, and the final argument list is shell-escaped with
shell-quotebefore being sent over SSH — user input is never interpolated into a shell string directly. The "Custom Command" tool is intentionally more open (it runs whatever binary/args you type) — treat it like a shell prompt into the Kali box. - Logging. The
webcontainer logs to stdout only (no log files to rotate or lose on a rebuild) - watch it withdocker compose logs -f web. Every line is tagged so you can grep for one kind:[ACCESS](every API request: method, path, status, IP, response time, signed-in user),[AUTH](login/registration/token events, including failed logins, OTP failures, and rate-limit hits - this is what to watch for brute-force or account-enumeration attempts), and[AUDIT](every command run, including each step of the automated scan and "Update Kali Tools" - username, tool, command, timestamp). - Panel-stored secrets (OIDC client secrets, SMTP password) are kept in
plaintext JSON files (
backend/data/oidc-providers.json,backend/data/smtp-config.json) on the same persistent volume as everything else — the same trust model as the SSH keypair on thessh-keysvolume (see above). There's no separate encryption-at-rest layer, so treat the host's Docker volumes/backups accordingly. - Wireless tools (aircrack-ng, reaver, etc.) need monitor-mode access to
a physical wireless NIC, which a standard Docker container doesn't have.
They're not wired up with forms here for that reason; you'd need to pass
through a USB Wi-Fi adapter (
--device) and run in a privileged/host-network configuration to use them. - Password-reset links and one-time codes are cryptographically random (a 256-bit token for reset links, a 6-digit code for sign-in), stored in memory only as a SHA-256 hash (never in plaintext, never written to disk), compared in constant time, single-use, short-lived (45 minutes for reset links, 10 minutes for codes), and the 6-digit code locks out after 5 wrong attempts. Both the request endpoints (which send an email) and the code-verification endpoint are separately rate-limited per IP on top of that. Requesting one for an account that doesn't exist, or that has no email on file, returns the exact same response as a real request — the login page can't be used to enumerate which accounts exist.
- SSH connection refused / auth failure from
webtokali: thekalicontainer generates its own keypair on first boot and shares it withwebover thessh-keysDocker volume - if this is a deployment upgraded from an older version that used a host-sidekeys/folder, remove that from your olddocker-compose.ymlbind mounts and let the new named volume take over;docker compose up -dwill recreate both containers with it. Checkdocker compose logs kalifor the actual SSH error if it still fails. - A tool's status dot is red: the configured
KALI_METAmetapackage doesn't include that binary. Install it manually (see above) or rebuild with a larger metapackage. - Locked out / forgot a password: see the recovery command in User management.
- "Sign in with X" fails or the IdP shows a redirect_uri error: the
registered redirect URI must match
<PUBLIC_URL or the URL you open the site at>/api/oidc/<key>/callbackexactly, including scheme and port. SetPUBLIC_URLin.envif you're behind a proxy or a non-default port, and checkdocker compose logs webfor the specific OIDC error.



