From Zero to Evidence: KaliGPT, Ollama, OpenCode, Kali MCP, Nmap, Nikto, and ZAP
Publishing note: www.xyz.com is an illustrative placeholder only. Replace it with a domain expressly authorized for testing before running network commands. Screenshots or results from other engagements are intentionally
Publishing note:
www.xyz.comis an illustrative placeholder only. Replace it with a domain expressly authorized for testing before running network commands. Screenshots or results from other engagements are intentionally excluded.
Authorized Web VAPT | A Complete Junior Operator Walkthrough
Document owner: Security Engineering / DevSecOps
Edition: 2.0 β 14 November 2026
Audience: Junior security analyst supervised by an authorized assessor
Platform: Kali Linux (tested lineage: Kali 2026; package-specific instructions)
Illustrative target only (authorization required): www.xyz.com
Mode: Human-approved, single-operation scans with native evidence files
Operating boundary: The example hostname is not a blanket authorization. Obtain written scope approval, a maintenance window, a point of contact, an approved source IP and a test account before scanning. Only TCP 80/443 are permitted in the first Nmap operation, and only HTTPS 443 in the Nikto baseline. Stop if a tool proposes additional ports, targets, scripts, brute force, destructive operations or a generic shell MCP action.
1. What you will build and how it works
This runbook contains two separate paths, not a single hidden chain. KaliGPT uses local Ollama for drafting and explanations; it does not automatically connect to the official Kali MCP bridge in the verified setup. OpenCode is the MCP client and approval interface. You can use KaliGPT to plan, but the controlled tool invocation happens from OpenCode, or via a human-run native scanner command.
PATH A β planning only
Operator -> KaliGPT (-o) -> Ollama -> local Qwen model
-> reviewer checks proposed commands -> NO automatic execution here
PATH B β human-approved MCP scan
Operator -> OpenCode V2 -> Ollama -> local Llama 3.1 8B
-> actual MCP tool invocation + Allow once
-> mcp-server (stdio bridge, started by OpenCode)
-> Kali API http://127.0.0.1:5000 (separate terminal)
-> Nmap / Nikto -> native evidence files
PATH C β authenticated web assessment (separate)
Operator -> ZAP proxy on Kali -> SSH local tunnel -> browser/test account
-> passive findings -> native ZAP HTML report
-> active scan ONLY by a new, separately approved test plan
Roles: The human defines scope, starts services, confirms parameters, approves one operation, examines the report and assigns findings. The model suggests a call but is not a security authority. The MCP bridge forwards a named tool invocation. The Kali API launches the scanner. The scanner writes native files. ZAP manages browser sessions separately. Do not rely on AI narrative as raw evidence.
Console ownership: T0 = setup/diagnostics Kali terminal; T1 = long-running Kali API process; T2 = OpenCode interface; T3 = evidence inspection Kali terminal; T4 = optional KaliGPT (not required for MCP); T5 = headless ZAP server; M1 = Mac SSH/browser for headless ZAP. Never paste commands intended for a Kali terminal into the OpenCode chat prompt.
2. Gate 0 β authorization and preflight
Before doing anything: record the JIRA/change ticket, named approver, exact authorized host, approved ports/HTTP methods, source EC2 IP, dates, scan budget, rollback contact, data-handling rules and whether authenticated ZAP testing is allowed. Confirm the environment is staging and that active scanning will not affect production, test payments or real customers. An ALB's multiple DNS A records do not grant permission to scan arbitrary IPs.
T0 β inventory the host. Run as a regular kali user; sudo is reserved for package installation and service management:
whoami
cat /etc/os-release | head -8
uname -m
free -h
df -h "$HOME"
ip -br addr
T0 β install baseline tools:
sudo apt update
sudo apt install -y git curl jq python3 python3-venv python3-pip \
openssl ca-certificates nmap nikto zaproxy mcp-kali-server
command -v nmap nikto zaproxy kali-server-mcp mcp-server
nmap --version | head -2
nikto -Version
kali-server-mcp -h
mcp-server -h
Pass criterion: all commands are found. If a package is unavailable, stop and reconcile Kali repositories; do not replace an official binary with a random downloaded implementation. Check network authorization before scanning (a connectivity test is not a VAPT finding):
getent ahostsv4 www.xyz.com | head -6
curl --noproxy '*' --max-time 10 --silent --show-error \
--head --max-redirs 0 "https://www.xyz.com/"
Expected HTTP codes depend on application routing: 200, 301, 302, 401, 403 or 404 can all show that TCP/TLS worked. A default 404 does not necessarily mean the application is down; for AWS ALB deployments it may reflect an unmatched route/SNI/host rule. If curl times out, first check the Kali EC2 egress IP, DNS, security group allowlist, routing and proxy environment; do not modify scan scope to work around a block.
3. Prepare the secure project and evidence directories
T0 β create one dedicated workspace. Commands below assume the default kali account home directory. Keep secrets outside the scan evidence tree and restrict access.
export PROJECT_ROOT="/home/kali/vapt/xyz-assessment"
export TARGET="www.xyz.com"
umask 077
mkdir -p "$PROJECT_ROOT"/{config,private,logs,reports}
mkdir -p "$PROJECT_ROOT"/evidence/{nmap,nikto,zap,validation}
chmod 700 "$PROJECT_ROOT"/private "$PROJECT_ROOT"/evidence
cd "$PROJECT_ROOT"
pwd
Create a short engagement record with authorized target and approval ID, not test-account passwords. Record versions and current timestamp in a text file, as a human-run operation:
{
printf 'Target: %s\n' "$TARGET"
date -u '+Recorded UTC: %Y-%m-%dT%H:%M:%SZ'
nmap --version | head -1
nikto -Version 2>&1 | head -3
dpkg-query -W mcp-kali-server
} > "$PROJECT_ROOT/logs/environment-baseline.txt"
4. Install and configure Ollama (local LLM runtime)
Why: Ollama runs the actual models. llama3.1:8b and qwen2.5:7b are model names; Ollama is the service. OpenCode and KaliGPT are separate applications using that runtime. Ollama downloads models from the vendor over the internet during setup; choose the locally installed models, not cloud variants, for the offline inference path.
T0 β inspect and install the official Linux script (review third-party installer content before execution; corporate package approval may be required):
cd "$HOME"
curl -fsSLo ollama-install.sh https://ollama.com/install.sh
less ollama-install.sh
# After security review/approval:
sh ollama-install.sh
sudo systemctl enable --now ollama
systemctl is-active ollama
curl --noproxy '*' -fsS http://127.0.0.1:11434/api/tags | jq .
ollama pull llama3.1:8b
ollama pull qwen2.5:7b
ollama list
ollama show llama3.1:8b
Sizing: for the observed 8B quantized Llama model, a 30 GiB RAM machine was adequate at 16K context while running on CPU; that is an observation, not a universal memory guarantee. On smaller hosts, choose a compatible model and validate tool calling before use. Monitor memory and disk space.
T0 β set Ollama to 16,384 context tokens:
sudo systemctl edit ollama.service
Insert exactly these two lines into the editor, save and exit:
[Service]
Environment="OLLAMA_CONTEXT_LENGTH=16384"
Apply and check:
sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama -p Environment | grep OLLAMA_CONTEXT_LENGTH
systemctl is-active ollama
Important: ollama ps displays active models only. If the list is empty, no model is currently loaded; it does not mean Ollama is broken. After OpenCode generates text, run ollama ps from another terminal and verify CONTEXT 16384. Context size can affect tool-calling reliability, but it is not a guarantee.
5. Install KaliGPT HackerX and use it safely
Purpose: KaliGPT is a separate command-line AI assistant. The upstream HackerX branch supports kaligpt -o to use Ollama and /change-model to select a model. The historical KaliGPT package used /opt/KaliGPT. Do not assume KaliGPT's own tools enforce OpenCode's Allow once approval. In this runbook KaliGPT is used for planning and explanations only.
T0 β clone and inspect the KaliGPT installer:
mkdir -p "$HOME/src"
cd "$HOME/src"
git clone --branch hackerx https://github.com/SudoHopeX/KaliGPT.git KaliGPT-setup
cd KaliGPT-setup
git rev-parse HEAD
less install.sh
less installers/installer.deb.sh
Check the installer for root operations, package downloads, where it clones the app, and permission changes. Do not blindly execute a network pipe. The upstream install.sh -m chooses the local Debian installer on Kali. Only after reviewing and approving it:
cd "$HOME/src/KaliGPT-setup"
sudo bash install.sh -m
command -v kaligpt
kaligpt --help
ls -ld /opt/KaliGPT
Version variability: installers can change upstream. If /opt/KaliGPT already exists, do not rerun a cloning installer over it. Confirm the installed version and actual configuration layout before modifying it. If KaliGPT asks whether to install Ollama again, choose No because Ollama is already installed. Prefer No for optional cloud API keys when using local models; do not paste company keys into KaliGPT.
T4 β start KaliGPT (optional, separate terminal):
kaligpt -o
Inside KaliGPT, use /change-model to select the installed qwen2.5:7b if the active default differs. The model in KaliGPT can differ from the model in OpenCode. Enter a planning-only prompt (text typed by the operator, not a shell command):
For the authorized host www.xyz.com, explain what a conservative
Nmap TCP 80/443 scan and Nikto HTTPS baseline would check.
Do not run commands, invoke tools, claim findings, or expand scope.
Return a human-reviewed checklist only.
Pass criterion: KaliGPT generates a plan, not evidence. If it claims to have scanned but no separately executed scanner result exists, do not accept the claim. Exit KaliGPT before running CPU-heavy OpenCode sessions on resource-limited hosts.
6. Install and configure the official Kali MCP API and bridge
Two different binaries: kali-server-mcp runs a Python/Flask API at 127.0.0.1:5000; mcp-server is the stdio bridge. OpenCode starts the bridge automatically when you launch its MCP integration. You do not manually launch a second bridge when OpenCode is using it.
T0 β verify package and executable paths:
dpkg-query -W mcp-kali-server
command -v kali-server-mcp mcp-server
kali-server-mcp -h
mcp-server -h
sudo ss -lntp | grep ':5000' || echo 'Nothing listening on 5000'
T1 β run one local-only Kali API server:
kali-server-mcp --ip 127.0.0.1 --port 5000
Leave T1 open. If Address already in use appears, do not start another server, kill random PIDs or bind a public address. Use T3 to identify the listener and check it:
sudo ss -lntp | grep ':5000'
curl --noproxy '*' -fsS --max-time 5 \
http://127.0.0.1:5000/health | jq .
Pass criterion: HTTP health JSON contains status: healthy and tools_status.nmap: true, tools_status.nikto: true. mcp-server may show as connected later even if port 5000 has died: verify both layers. Do not expose ports 5000 or 11434 through the EC2 security group.
7. Install OpenCode V2 and configure its model, MCP server and approvals
T0 β download and inspect the official V2 installer (use the V2 entry point; an older installer may install the wrong version):
cd "$HOME"
curl -fsSLo opencode-v2-install.sh https://opencode.ai/v2/install
less opencode-v2-install.sh
# After review:
bash opencode-v2-install.sh
export PATH="$HOME/.opencode/bin:$PATH"
hash -r
command -v opencode
opencode --version
If installed outside ~/.opencode/bin, use the exact command -v opencode path. For future interactive sessions, append the PATH line to the user's shell startup file (for Kali's default zsh, ~/.zshrc; for Bash, ~/.bashrc) and reopen the shell:
printf '\nexport PATH="$HOME/.opencode/bin:$PATH"\n' >> "$HOME/.zshrc"
T0 β create a deliberately restrictive project-local opencode.json. This is OpenCode V2 syntax: mcp.servers, permissions list, actions shell and named kali_... tools, with the last matching permission rule winning. Do not use legacy V1 mcpServers, permission, or bash keys.
cd "/home/kali/vapt/xyz-assessment"
cat > opencode.json <<'JSON'
{
"$schema": "https://opencode.ai/config.json",
"model": "ollama/llama3.1:8b",
"mcp": {
"servers": {
"kali": {
"type": "local",
"command": [
"/usr/bin/mcp-server", "--server", "http://127.0.0.1:5000",
"--timeout", "900"
],
"codemode": false
}
}
},
"permissions": [
{ "action": "*", "resource": "*", "effect": "deny" },
{ "action": "kali_server_health", "resource": "*", "effect": "ask" },
{ "action": "kali_nmap_scan", "resource": "*", "effect": "ask" },
{ "action": "kali_nikto_scan", "resource": "*", "effect": "ask" },
{ "action": "read", "resource": "evidence/*", "effect": "allow" }
],
"policies": [
{ "action": "tool.use", "resource": "shell:*", "effect": "deny" },
{ "action": "tool.use", "resource": "kali_execute_command:*", "effect": "deny" }
]
}
JSON
chmod 600 opencode.json
jq empty opencode.json
--timeout 900 is the bridge request timeout. It is not Nikto's runtime and does not extend the Kali API process timeout. No custom plugin is installed here; scanners write their native output. The permissions limit which OpenCode tools may run, but cannot by themselves validate hostnames, port lists or side effects inside all MCP tool parameters; the human reviewer must inspect the request.
T0 β export the specific config and verify MCP:
cd "/home/kali/vapt/xyz-assessment"
export OPENCODE_CONFIG="$PWD/opencode.json"
echo "$OPENCODE_CONFIG"
opencode mcp list
Expected: kali connected. If it says none, check the environment variable, working directory, JSON validity and V2 path. If connected but /health fails, troubleshoot the Kali API in T1, not OpenCode.
T2 β open OpenCode:
cd "/home/kali/vapt/xyz-assessment"
export OPENCODE_CONFIG="$PWD/opencode.json"
opencode
Confirm the selected model in OpenCode is ollama/llama3.1:8b. New deployments may display slightly different UI labels. Start a fresh conversation for each trial; long, repetitive prompts can cause smaller local models to print fake JSON instead of calling tools.
8. First real MCP test β health-check and approval workflow
T2 β paste the following prompt in OpenCode chat (not the Linux terminal):
Invoke the real kali_server_health MCP tool exactly once.
Use the OpenCode tool interface, not printed JSON or a simulated call.
Show the actual permission dialog. Execute only after I choose Allow once.
Return the actual health result and stop. Do not run any scanners.
Operator checkpoint: Verify the UI shows an actual tool invocation for kali_server_health. Select Allow once (never Always allow for this procedure). If OpenCode merely prints {"name":"kali_server_health"...}, that is not an execution. If there is no permission dialog, investigate which rule matched.
T3 β independent API check:
curl --noproxy '*' -fsS --max-time 5 \
http://127.0.0.1:5000/health | jq .
Pass criterion: status: healthy and tools_status reflects the installed scanners. In an MCP result status: completed means the tool call finished; always inspect nested scanner success, return_code, timed_out, and actual stdout. OpenCode's prose is not proof of the underlying process status.
9. Nmap baseline β one approved host and native evidence
Objective: discover states/service responses on TCP 80 and 443 only. -sT -sV -T2 --max-retries 1 --max-rate 5 is an intentionally conservative TCP connect/version scan. Service probes may return 404, which is not itself a vulnerability. DNS names backed by an ALB may resolve to more than one IP; Nmap typically selects one IP in one invocation. Do not silently expand to unapproved IPs.
T2 β OpenCode prompt, pasted by the human:
Invoke the real kali_nmap_scan MCP tool exactly once.
Target: www.xyz.com
ports: 80,443
scan_type: -sT -sV
additional_args: -T2 --max-retries 1 --max-rate 5 -oA /home/kali/vapt/xyz-assessment/evidence/nmap/nmap-baseline-01
Use only the named Nmap MCP tool. Before execution show the exact real tool
name, target and parameters in OpenCode's Allow once / Reject dialog.
Execute only after I select Allow once. No NSE, OS detection, UDP, other
ports, other hosts, shell commands or additional tools. The native Nmap
-oA option must write files; do not ask the model to rewrite scanner output.
Afterwards report actual completion status and stop.
T3 β find Nmap-native evidence files:
cd "/home/kali/vapt/xyz-assessment"
ls -lh evidence/nmap/nmap-baseline-01.*
test -s evidence/nmap/nmap-baseline-01.nmap && echo 'Normal output exists'
test -s evidence/nmap/nmap-baseline-01.xml && echo 'XML output exists'
test -s evidence/nmap/nmap-baseline-01.gnmap && echo 'Grepable output exists'
sed -n '1,100p' evidence/nmap/nmap-baseline-01.nmap
sha256sum evidence/nmap/nmap-baseline-01.* > evidence/nmap/nmap-baseline-01.sha256
If the native files do not exist, investigate the actual call arguments and stdout; do not have the LLM reconstruct the output. The .xml is suitable for downstream parsing and the .nmap file is the operator-readable report.
Optional fallback β direct native CLI (T0, only as a separately approved operation, not in addition to the MCP scan without authorization):
cd "/home/kali/vapt/xyz-assessment"
nmap -sT -sV -T2 --max-retries 1 --max-rate 5 \
-p 80,443 -oA evidence/nmap/nmap-native-fallback-01 \
www.xyz.com
echo "Nmap exit status: $?"
Pass criterion: exact hostname/ports, Nmap done without terminating error, expected three files, sensible port/service interpretation and a preserved checksum. 80/443 open is exposure, not a confirmed vulnerability.
10. Nikto web-server baseline β native HTML and MCP timeout handling
Scope: unauthenticated HTTPS 443 on exactly one hostname. Nikto can identify server/HTTP-layer observations, not authenticated RBAC or business logic. Start with -Tuning 23, which checks misconfiguration/default files and information disclosure; expand to other categories only after an explicit change approval. Nikto may make many HTTP requests and may encounter sensitive paths. Do not enable unreviewed intrusive plugins or redirects to other hosts.
Important syntax correction: do not use a full https://... URI together with -port 443; the observed Nikto 2.6.1 returned The -port option cannot be used with a full URI. Use the bare hostname plus -port 443 -ssl.
T2 β conservative approved Nikto prompt:
Invoke the real kali_nikto_scan MCP tool exactly once.
target: www.xyz.com
additional_args: -port 443 -ssl -Tuning 23 -Cgidirs none -Pause 0.2 -maxtime 600s -timeout 10 -nointeractive -nocheck -Format htm -output /home/kali/vapt/xyz-assessment/evidence/nikto/nikto-baseline-01.htm
Require an actual named-tool permission dialog. Run only after Allow once.
Do not scan another hostname or port, follow redirects to unrelated hosts,
use credentials, run a shell tool, or start other scanners.
Nikto itself must write the native HTML file; do not ask the AI to recreate it.
Report actual success, return_code, timed_out, stderr, findings and whether
Nikto reported a normal completion. Stop after one scan.
T3 β verify native HTML:
cd "/home/kali/vapt/xyz-assessment"
ls -lh evidence/nikto/nikto-baseline-01.htm
file evidence/nikto/nikto-baseline-01.htm
test -s evidence/nikto/nikto-baseline-01.htm && echo 'HTML present'
sha256sum evidence/nikto/nikto-baseline-01.htm \
> evidence/nikto/nikto-baseline-01.htm.sha256
sha256sum -c evidence/nikto/nikto-baseline-01.htm.sha256
Why only -Format htm: an earlier run with -Format htm,json created a usable HTML file but an empty 0-byte JSON file. Generate a single native HTML in this SOP and use the MCP API result or native terminal text for completion evidence. If JSON is needed, run a format-export check separately; don't claim an empty JSON as a report.
Optional direct native Nikto CLI, only if permitted and only as a separate approved run:
cd "/home/kali/vapt/xyz-assessment"
nikto -host www.xyz.com -port 443 -ssl \
-Tuning 23 -Cgidirs none -Pause 0.2 -maxtime 600s -timeout 10 \
-nointeractive -nocheck -Format htm \
-output evidence/nikto/nikto-native-fallback-01.htm
printf 'Nikto CLI exit status: %s\n' "$?"
Completion gate: do not equate success: true with scan success. The API can return success: true, return_code: -1, timed_out: true, because it preserved partial output. Confirm scan elapsed time, normal End Time, request/error summary and no external timeout. A five-minute/ten-minute scanner duration does not imply full VAPT coverage.
11. If Nikto is cut off at 180 seconds: approved, project-local fix
Only apply this section after proving the 180-second Kali API limit. This exact bug was observed in package line mcp-kali-server 0.0~git20260317.00154c0-0kali3, whose installed /usr/share/mcp-kali-server/server.py contained COMMAND_TIMEOUT = 180 # 5 minutes default timeout (comment is wrong). Enlarging only -maxtime 10000s did not help; the API set timed_out: true after ~180 seconds. Do not blindly patch other versions.
T0 β inspect the installed code first:
dpkg-query -W mcp-kali-server
sudo grep -nE 'COMMAND_TIMEOUT|class CommandExecutor|def nikto|result = execute_command' \
/usr/share/mcp-kali-server/server.py
sudo sed -n '25,55p' /usr/share/mcp-kali-server/server.py
sudo sed -n '270,320p' /usr/share/mcp-kali-server/server.py
Gate: confirm the Nikto route calls result = execute_command(command) exactly once and CommandExecutor(..., timeout=...) has an .execute() method. If layout differs, stop and get a reviewer to update the patch. The command below creates an independent copy; the system package is unchanged.
cd "/home/kali/vapt/xyz-assessment"
python3 - <<'PY'
from pathlib import Path
import os
src = Path('/usr/share/mcp-kali-server/server.py')
dst = Path('local-kali-api/server.py')
s = src.read_text()
start = s.find('def nikto():')
end = s.find('\[email protected](', start + 1)
if start < 0 or end < 0:
raise SystemExit('Unknown API layout; no change')
block = s[start:end]
old = 'result = execute_command(command)'
new = 'result = CommandExecutor(command, timeout=720).execute()'
if block.count(old) != 1 or 'class CommandExecutor' not in s:
raise SystemExit('Unexpected Nikto handler; no change')
dst.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
s2 = s[:start] + block.replace(old, new, 1) + s[end:]
compile(s2, str(dst), 'exec')
dst.write_text(s2)
os.chmod(dst, 0o600)
print('Created verified local file:', dst.resolve())
PY
T1 β switch servers without port collision. First stop the old API using Ctrl+C in its original terminal (after ensuring no scan is in progress). If launched by a supervisor, identify the owner/service and stop using its approved procedure; do not kill unknown processes. Verify port 5000 is free before starting the local copy:
sudo ss -lntp | grep ':5000' || echo 'Port 5000 free'
T1 β start the locally patched API:
cd "/home/kali/vapt/xyz-assessment"
python3 local-kali-api/server.py --ip 127.0.0.1 --port 5000
T3 β check both API health and actual process:
curl --noproxy '*' -fsS http://127.0.0.1:5000/health | jq .
ps aux | grep '[l]ocal-kali-api/server.py'
The health endpoint alone cannot distinguish patched from unpatched server. Preserve opencode.json bridge --timeout 900, keep Nikto's own -maxtime 600s, and use one new output filename for each trial. This patch has to be re-evaluated when upgrading the Kali package, and it changes only the Nikto route in the local copy. If a local run uses a different source layout or fails health, restore by stopping it and restarting the original kali-server-mcp --ip 127.0.0.1 --port 5000. Never edit /usr/share/mcp-kali-server/server.py in place as the default procedure.
12. OWASP ZAP β headless, passive authenticated validation and native HTML
Scope and status: ZAP is installed with Kali packages. A headless Kali environment can have DISPLAY=not-set, so GUI commands on that Kali SSH session would fail. ZAP authentication and active scan results were not validated in the historical session. Use this as a controlled, step-by-step setup, not as evidence that an authenticated assessment was already performed. ZAP is not part of the tested Kali MCP named-tool set; start it separately.
T0 β verify installation and display:
command -v zaproxy
echo "DISPLAY=${DISPLAY:-not-set}"
zaproxy -version
Credentials: use dedicated staging-only test users with synthetic data. The login is entered in the proxied browser, not in KaliGPT, OpenCode or an MCP tool call. ZAP sessions include cookies, bearer tokens and potentially personal data; protect them. Do not copy ZAP session DB files into public Confluence pages. Password changes, logout, destructive methods and out-of-scope identity providers must be excluded from active testing.
T0 β prepare a local ZAP API key and session (no key in shell command line):
cd "/home/kali/vapt/xyz-assessment"
umask 077
read -rs -p 'Enter a strong dedicated ZAP API key: ' ZAP_API_KEY; echo
printf 'api.key=%s\n' "$ZAP_API_KEY" > private/zap.properties
chmod 600 private/zap.properties
T5 β start local-only ZAP (do not reuse the long-running Kali API terminal):
cd "/home/kali/vapt/xyz-assessment"
zaproxy -daemon -host 127.0.0.1 -port 8090 \
-configfile "$PWD/private/zap.properties" \
-newsession "$PWD/evidence/zap/zap-session-01"
Keep running. Do not add port 8090 to an AWS security group. If it is already running, identify the listener instead of launching another daemon. Verify the listening socket:
sudo ss -lntp | grep ':8090'
M1 β from a Mac, open an SSH tunnel to Kali (separate Mac terminal):
ssh -N -L 8090:127.0.0.1:8090 \
-i ~/.ssh/REPLACE_WITH_APPROVED_EC2_KEY.pem \
kali@REPLACE_WITH_KALI_EC2_SSH_HOST
T3 β export only the public ZAP root CA certificate (not its private signing key). Use the same key entered at startup; the API may require it even for certificate export. Do this before browser configuration:
cd "/home/kali/vapt/xyz-assessment"
read -rs -p 'Enter the existing ZAP API key: ' ZAP_API_KEY; echo
curl --noproxy '*' --silent --show-error --get \
'http://127.0.0.1:8090/OTHER/core/other/rootcert/' \
--data-urlencode "apikey=$ZAP_API_KEY" \
--output private/zap-root-ca.cer
file private/zap-root-ca.cer
# Certificate encoding varies; check DER, then PEM if needed:
openssl x509 -inform DER -in private/zap-root-ca.cer \
-noout -subject -fingerprint -sha256 || \
openssl x509 -in private/zap-root-ca.cer \
-noout -subject -fingerprint -sha256
M1 β copy the CA from Kali to your Mac (Mac terminal, replace host/key):
scp -i ~/.ssh/REPLACE_WITH_APPROVED_EC2_KEY.pem \
kali@REPLACE_WITH_KALI_EC2_SSH_HOST:/home/kali/vapt/xyz-assessment/private/zap-root-ca.cer \
"$HOME/Downloads/zap-root-ca.cer"
On the Mac, create a dedicated Firefox test profile. Configure its HTTP and HTTPS proxy to 127.0.0.1 port 8090 and do not reuse personal browser profiles. In Firefox's Certificates settings, open View Certificates β Authorities β Import, select the downloaded CA and trust it only for this disposable testing profile. Check the certificate fingerprint against the export output, then remove the test profile and its certificate after the engagement. To avoid intercepting unrelated browsing, use this profile only for the authorized target. Test proxy routing before login and stop if a certificate warning is unexplained.
Manually authenticate: visit https://www.xyz.com/ in the test profile; enter dedicated staging credentials in the actual login form. If authentication redirects to an unrelated SSO domain, pause and establish whether that traffic is permitted (proxying a login is not the same as authorizing a scan of the IdP). Browse only harmless, approved application functions. Do not use ZAP Quick Start Automated Scan here: it can spider and perform active attack tests.
ZAP passive evidence procedure: keep ZAP in Safe mode for browsing; passive scanning processes the captured traffic. Verify ZAP's history/Sites view has authenticated requests for the exact host and do not assume a login succeeded merely because a cookie exists. For manual GUI access later, use ZAP Report -> Generate Report, select only the authorized site and save native HTML under evidence/zap/. On the headless server, use the API only after verifying installed Report Generation add-on and available templates.
T3 β list API report templates (in the same shell in which ZAP_API_KEY was entered above; if using a new shell, enter the same key again):
curl --noproxy '*' --silent --show-error --get \
'http://127.0.0.1:8090/JSON/reports/view/templates/' \
--data-urlencode "apikey=$ZAP_API_KEY" | jq .
T3 β create a native scoped HTML report once the site has appeared in ZAP's Sites tree:
cd "/home/kali/vapt/xyz-assessment"
curl --noproxy '*' --silent --show-error --get \
'http://127.0.0.1:8090/JSON/reports/action/generate/' \
--data-urlencode "apikey=$ZAP_API_KEY" \
--data-urlencode 'title=Authorized Passive Web Review' \
--data-urlencode 'template=traditional-html' \
--data-urlencode 'sites=https://www.xyz.com' \
--data-urlencode 'reportFileName=zap-passive-01.html' \
--data-urlencode "reportDir=$PROJECT_ROOT/evidence/zap" \
--data-urlencode 'display=false' | jq .
ls -lh evidence/zap/zap-passive-01.html
sha256sum evidence/zap/zap-passive-01.html \
> evidence/zap/zap-passive-01.html.sha256
Caveat: ZAP's API template selection and the exact site string must be checked on your installed add-ons. If no report is generated, inspect the API JSON error; do not guess a report path or disable the API key. The report may contain sensitive HTTP context, so review/redact before distribution. An active scan is not part of this default workflow. For active tests: acquire separate approval, create a strict Context on the exact hostname, exclude logout/changes/payment/out-of-scope redirects, use Protected mode, enforce a rate-limited policy, test with a dedicated account and monitor application behavior.
13. False positives and evidence-backed interpretation
Rule: Nikto descriptions and ZAP alerts are hypotheses. Treat /.htpasswd, /.bash_history, /.sh_history, *.json and suspected admin pages as unverified until real HTTP responses prove exposure. Applications and ALBs often return the same generic 200/404 fallback for arbitrary paths, which can trigger false positives. Do not retrieve full credential-file or shell-history contents just to populate a report.
T3 β initial HEAD-only comparison (explicitly authorized exact hostname):
HOST='www.xyz.com'
CONTROL="/.nonexistent-review-$(date +%s)-$$"
for p in "$CONTROL" '/.htpasswd' '/.bash_history' '/.sh_history'; do
printf '\n=== %s ===\n' "$p"
curl --noproxy '*' --silent --show-error --head \
--max-time 10 --max-redirs 0 "https://${HOST}${p}" \
| grep -Ei '^(HTTP/|server:|content-type:|content-length:|location:)'
sleep 1
done
Interpretation: identical 404/401/403 to control is usually not confirmed public exposure; distinct 200 warrants authorized content-type/response verification by the asset owner, not an automatic Critical. HEAD behavior may differ from GET, so it is only a triage step. Missing security headers must be tested on the actual application response (not default ALB 404). A wildcard certificate is not automatically a weakness. A reported nginx version needs inventory/vendor patch evidence; version banners alone do not prove exploitability.
Validation register: for each finding record Reported title, URL/path, HTTP method, scanner evidence, control comparison, affected component, manual reproduction, status (Confirmed / False Positive / Informational / Needs Validation), severity rationale, remediation owner, JIRA ticket, review date. Avoid severity or CVSS until impact is substantiated.
14. Locate, read, integrity-check and share scanner reports
T3 β inventory all native outputs:
cd "/home/kali/vapt/xyz-assessment"
find evidence -maxdepth 3 -type f -printf '%p | %s bytes\n' | sort
file evidence/nikto/*.htm 2>/dev/null || true
View: Open .htm reports in a browser; view .nmap text in a terminal; import .xml into an approved parser. For a headless Kali instance, transfer with SCP from the Mac (replace SSH identity/host):
scp -i ~/.ssh/REPLACE_WITH_APPROVED_EC2_KEY.pem \
kali@REPLACE_WITH_KALI_EC2_SSH_HOST:/home/kali/vapt/xyz-assessment/evidence/nikto/nikto-baseline-01.htm \
"$HOME/Downloads/"
open "$HOME/Downloads/nikto-baseline-01.htm"
Check integrity: SHA-256 proves consistency with the recorded hash, not scanner truthfulness or authenticated chain of custody. Do not hash an empty/missing file and call it evidence.
cd "/home/kali/vapt/xyz-assessment"
find evidence -type f -name '*.sha256' -print
sha256sum -c evidence/nikto/nikto-baseline-01.htm.sha256
Report acceptance checklist: exact approved target and IP selected, tool/version, command/options, UTC start/end, exit code/timeout, request and error counts, scope, native raw output, original checksum, analyzed findings with reproducible evidence, sensitive-data review and explicit assessment limitations. The analyst produces a client-facing report only after false-positive review; the scanner HTML is supporting evidence, not a finished VAPT opinion.
Example scenario β end-to-end junior walkthrough
Assume your assessor has explicitly authorized a staging replica reachable as www.xyz.com, with TCP 80/443, Nikto HTTPS-only and passive ZAP after login. First install Kali packages and the two model interfaces, set the Ollama context, and obtain a passing curl ... /health. Second generate project-local OpenCode V2 config and verify kali connected. Third ask for the real kali_server_health call, then separately approve the Nmap prompt and check the native .nmap/.xml/.gnmap reports. Fourth approve the narrow Nikto prompt and open the native .htm. Finally, proxy a dedicated test browser via SSH into ZAP, authenticate only with non-production credentials, browse harmless pages in Safe mode, and generate the native ZAP report. Escalate manual false-positive questions, verify hashes, and sign the scope/evidence checklist before publication. If any stage fails, stop and use the matching troubleshooting subsection rather than moving to the next scanner.
15. Daily operations β minimal complete command sequence
Every engagement day, use this exact order. Do not skip verification because the CLI looks connected.
T0 β current state:
cd "/home/kali/vapt/xyz-assessment"
export OPENCODE_CONFIG="$PWD/opencode.json"
export PROJECT_ROOT="$PWD"
export TARGET='www.xyz.com'
systemctl is-active ollama
curl --noproxy '*' -fsS http://127.0.0.1:11434/api/tags | jq '.models[].name'
sudo ss -lntp | grep ':5000' || echo 'Kali API not running'
curl --noproxy '*' -fsS --max-time 5 http://127.0.0.1:5000/health | jq .
opencode mcp list
T1 β if API not running, start exactly one approved API binary, original or project-local patched variant (see Section 11), and keep it running. T2 β launch:
cd "/home/kali/vapt/xyz-assessment"
export OPENCODE_CONFIG="$PWD/opencode.json"
opencode
T2 β first call: health tool with human Allow once. T2 β second call: separately approved Nmap or Nikto prompt from Sections 9β10, exactly one per action. T3 β verify native files, process completion and hashes (Sections 9β10 and 14). T4 β optional kaligpt -o for planning only, not simultaneously substituting for the MCP client. M1/ZAP β separate passive authenticated workflow (Section 12). Closeout: classify results, write JIRA tickets for verified issues, store restricted evidence, stop temporary local processes and restore any security-group temporary allowlist according to the change ticket.
16. Troubleshooting β symptoms, causes, exact checks, corrections
T01. opencode mcp list shows none
Cause: exported OPENCODE_CONFIG missing/wrong, wrong working directory, malformed JSON, or V1-format MCP config.
cd "/home/kali/vapt/xyz-assessment"
export OPENCODE_CONFIG="$PWD/opencode.json"
echo "$OPENCODE_CONFIG"
jq '.mcp.servers.kali' "$OPENCODE_CONFIG"
opencode mcp list
Correct configuration to mcp.servers.kali and local command ['/usr/bin/mcp-server', ...]; do not use V1 mcpServers.
T02. kali connected but tool reports connection refused on port 5000
Cause: bridge is connected, downstream API died or never started.
sudo ss -lntp | grep ':5000' || echo 'Stopped'
curl --noproxy '*' -i --max-time 5 http://127.0.0.1:5000/health
Start/restore kali-server-mcp in T1. Do not reinstall OpenCode, change model or run Nmap again before API health is confirmed.
T03. Address already in use on 127.0.0.1:5000
Cause: an existing API is listening. Identify with sudo ss -lntp | grep ':5000'; verify health with curl. If healthy, reuse it. To switch from system package to the project-local Nikto timeout copy, stop the known original server via Ctrl+C and check port free before starting local copy. Never issue blind pkill python or expose port to 0.0.0.0.
T04. Ollama empty ollama ps / context 4096
Cause: model unloaded when idle; or systemd override not applied. Inspect systemctl is-active ollama, systemctl show ollama -p Environment, run OpenCode, and inspect ollama ps while active. Set OLLAMA_CONTEXT_LENGTH=16384, restart Ollama and verify 16384. Model still may be unreliable; a higher context is not proof of reliable tool calling.
T05. Llama prints JSON / tools.opencode.execute(...) but no tool dialog
Cause: model-generated text, not an actual tool call. Check real UI named tool and Allow once. Start a fresh OpenCode session; test kali_server_health first; verify MCP list and Ollama context. Never treat model-generated JSON as scan evidence or approve an arbitrary fake invocation.
T06. MCP status: completed and success: true despite timed_out: true
Cause: response indicates tool returned, perhaps with partial stdout, but scanner was terminated. Check return_code, timed_out, stderr, start/end timestamps and native files. return_code=-1 plus timed_out=true was the decisive failure signature. Do not report zero vulnerabilities or completed coverage.
T07. Nikto maximum execution limit did not increase with -maxtime 10000s
Cause: installed Kali API's hardcoded COMMAND_TIMEOUT=180 wins. Inspect /usr/share/mcp-kali-server/server.py. If matching this package version, use the Section 11 local-copy patch, Nikto 600, API 720, bridge 900, and a fresh report filename. Never assume a longer -maxtime solved the API timeout.
T08. Nikto says -port option cannot be used with a full URI
Correction: supply -host www.xyz.com -port 443 -ssl; not -host https://www.xyz.com -port 443.
T09. Nikto HTML exists but JSON is 0 bytes
Cause: simultaneous output formats failed/partially worked in the historical environment. Use one -Format htm native output per run; verify file, test -s, sha256sum. A zero-byte JSON file is not valid evidence.
T10. Nmap/HTTPS returns ALB 404 or nginx/awselb banners change
Cause: routing/SNI/Host/load balancer/default handler, possibly multiple backend addresses. Re-check real hostname request, avoid IP substitution, compare actual app response and default errors. A changing banner is not itself a vulnerability. Do not claim nginx unpatched from the banner alone.
T11. Unexplained 200 on /.htpasswd / sensitive files
Cause: may be generic SPA fallback or error page. Compare status, length, content-type with random nonexistent control; do not dump secrets. If distinct, escalate to asset owner for controlled validation. Label unresolved cases Needs Validation, not Confirmed.
T12. Kali source times out before a scan
Cause: DNS/routing/ALB security group doesn't allow assessment EC2 egress IP. Verify source public IP via approved mechanism, route and load balancer inbound rules. A temporary allowlist change requires ticket and removal plan; don't make the app publicly accessible to fix it.
T13. ZAP DISPLAY=not-set / GUI cannot launch
Cause: headless SSH host. Start ZAP with -daemon, bind 127.0.0.1:8090, SSH tunnel from Mac. Do not disable API key or expose the proxy publicly. For browser login, use dedicated test profile and a scoped Context.
T14. API version or package differs from this runbook
Correction: run dpkg-query -W mcp-kali-server, opencode --version, ollama --version, nikto -Version, and inspect server.py. Do not apply a string-replacement patch to unrecognized source. Resolve differences with a reviewed change and document version numbers.
JIRA finding and Confluence review template
For every validated candidate, create a separate JIRA issue only after analyst review. The below is a copyable text template (not a scanner command); do not paste confidential credential data in the ticket.
Summary: [Affected service] β [specific verified behavior]
Environment and exact hostname:
Assessment scope/ticket ID:
Scanner/tool and native evidence filename:
UTC timestamp and specific URL/path/method:
Reproduction (non-destructive; actual observed status/body indicators):
Control request (nonexistent path comparison):
Security impact and affected component:
Likelihood/confidence and severity rationale:
False-positive evaluation:
Recommended remediation and owner:
Acceptance/retest criteria:
Evidence checksum and restricted storage location:
Validation status: Confirmed / False Positive / Informational / Needs Validation
Link a Confluence review page to the approved JIRA ticket and add the supervisor's sign-off date. Do not attach raw authenticated ZAP sessions or sensitive HTTP bodies to broadly visible tickets.
17. Analyst closeout and sign-off
The junior analyst must hand over: scope approval ID; current architecture and running process list; .nmap/.xml/.gnmap, .htm and scoped ZAP .html files; timestamps; checksum verification; scan completeness; an issues register with reproducible/false-positive status; and explicit unknowns. No finding may be called confirmed based on a model summary, version string or unverified resource-name match alone. Supervisor signs the finding review before Confluence/JIRA publication.
18. Sources and version notes
These references verify tool behavior, not the target's security posture. Date of review: 14 November 2026.
- Official Kali package documentation: https://www.kali.org/tools/mcp-kali-server/
- Kali MCP reference implementation (version-dependent): https://github.com/Wh0am123/MCP-Kali-Server/blob/main/server.py
- KaliGPT HackerX upstream install/CLI: https://github.com/SudoHopeX/KaliGPT/tree/hackerx
- Ollama Linux install: https://ollama.com/download/linux
- OpenCode V2 install: https://opencode.ai/docs/
- OpenCode V2 permissions and tool actions: https://opencode.ai/docs/permissions/
- OpenCode MCP servers: https://opencode.ai/docs/mcp-servers/
- OpenCode local Ollama models: https://opencode.ai/docs/models/
- Nikto 2.6.1 documentation: https://github.com/sullo/nikto/wiki
- ZAP Automation Framework: https://www.zaproxy.org/docs/automate/automation-framework/
- ZAP Report Generation API: https://www.zaproxy.org/docs/desktop/addons/report-generation/api/
- ZAP Safe/Protected modes: https://www.zaproxy.org/docs/desktop/start/features/modes/
Known versus unknown: The commands and fixed paths follow the tool versions discussed here. Authenticated ZAP and any extended Nikto service lifecycle must be validated locally. A report is not evidence of 100% vulnerability coverage.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
