Kasten FLR UI — File Level Recovery, Malware Detection & Scheduled Scanning
A full-featured, demo-grade web application for Kasten K10's native File-Level Recovery (FLR) API, extended with integrated on-demand malware scanning and automated scheduled scanning of all backup policies. Provides a guided six-step wizard to browse backup contents and recover individual files or folders — directly into running application pods, to any SSH host, or as a ZIP download to the browser.
Built with FastAPI + vanilla JS on Alpine Linux, deployed on OpenShift in its own namespace.
Written by James Tate — Kasten Senior System Engineer — 2026
https://github.com/jdtate101/kasten-flr-ui
Table of Contents
- Architecture
- Features
- Prerequisites
- File Structure
- Initial Deployment
- Update & Redeploy
- Recovery Workflow
- Malware Scanning
- Scheduled Malware Scanner
- YARA Rules Management
- Snapshot vs Export Restore Points
- Authentication
- Session History
- Health Dashboard
- Technical Notes
- Environment Variables
- RBAC Summary
- Kasten FLR API Reference
Architecture
Browser
│
▼
nginx:8080 ──────────────────────────────────────────────────────────────
│ │
▼ │
FastAPI:8000 │
│ │
├── Kubernetes API (https://kubernetes.default.svc) │
│ ├── GET /apis/config.kio.kasten.io → Policies │
│ ├── GET /apis/apps.kio.kasten.io → RestorePoints │
│ ├── POST /apis/datamover.kio.kasten.io → FileRecoverySession CR │
│ ├── POST /apis/actions.kio.kasten.io → RestoreAction (scan) │
│ ├── GET /api/v1/namespaces → Namespace list │
│ └── GET /api/v1/pods → Pod list + exec │
│ │
├── kubectl port-forward ──► FLR SFTP pod (kasten-io:2222) │
│ └── paramiko SFTP client ──► Browse & stage files │
│ │
├── kubectl cp ──► Running pod filesystem (pod recovery) │
├── paramiko SCP ──► Remote SSH host (SSH recovery) │
├── ZIP + stream ──► Browser download │
│ │
├── Malware scan namespace (kasten-malware-scan-<id>) │
│ ├── RestoreAction → clone PVCs into isolated namespace │
│ ├── Scale workloads to 0 → release PVC mounts │
│ ├── YARA rules ConfigMap → injected from /data/yara-rules/ │
│ └── Scanner Job (malware-scanner image) → YARA + ClamAV │
│ │
└── asyncio background scheduler │
├── Wakes every 60s → checks /data/scan-schedule.json │
├── Iterates all eligible policies sequentially │
├── Uses snapshot RP (falls back to export) per policy │
└── Writes to /data/scan-history.json + optional email alert │
│
Static files (index.html, app.js, scan.js, schedule.js, │
style.css, login.html, veeam-logo.png) ◄────────────────────┘
Why kubectl port-forward for SFTP?
Kasten's FLR NetworkPolicy only allows SFTP ingress from the application's own namespace (e.g. navidrome). Since the app pod runs in kasten-flr-ui, direct TCP to the FLR pod is blocked. kubectl port-forward tunnels through the Kubernetes API server, which is not subject to NetworkPolicy.
Features
Core Recovery
| Feature | Detail |
|---|---|
| Policy browser | Lists all Kasten backup policies with real-time search/filter by name or namespace. Skeleton shimmer placeholders while loading. Excludes KubeVirt/VM policies and policies with no PVC data |
| Restore point selection | Snapshot and export RPs with type badges. Paired RPs from the same backup run grouped side-by-side with green left border. Skeleton loading on Step 2 |
| Step 3 confirmation card | Policy avatar, restore point name, type badge, PVC list, mount headroom warning if capacity is low |
| FLR session lifecycle | Creates FileRecoverySession CR, SSE heartbeat polling until Ready, SFTP connection, session expiry countdown. Click badge to get terminate dropdown |
| Dual-pane file browser | SFTP browser with 60+ file-type-accurate SVG icons. Hover tooltips show file size, modified date and full path. Drag-and-drop or checkbox selection basket |
| Pod recovery | kubectl cp into a running pod with an in-browser filesystem navigator to select destination path |
| SSH recovery | SCP files to any SSH host |
| Browser ZIP download | Download selected files as a ZIP directly to the browser |
| Transfer success animation | Spring-animated green banner with checkmark on completion |
| Real-time progress | SSE-streamed colour-coded log window with progress bar |
Malware Scanning
| Feature | Detail |
|---|---|
| Pre-recovery scan | Scan any restore point via "Scan for Malware" in Step 2 before committing to recovery |
| Isolated clone namespace | RestoreAction clones PVCs into kasten-malware-scan-<id>, workloads scaled to 0 to release PVC mounts |
| Dual-engine scanning | YARA (user-defined rules) + ClamAV (freshclam updates signatures at scan start, 60s timeout, falls back to baked-in) |
| All PVCs scanned | Every PVC in the restore point must pass — a single threat marks the whole restore point dirty |
| SSE streaming | Live restore progress bar, per-PVC status pills, threat hits appear in real time |
| Animated result banner | Full-width spring-animated CLEAN (green) or DIRTY (red) banner on completion |
| Threat detail | Scanner, rule name, file path per threat, with MITRE ATT&CK / CISA reference links for known malware families |
| Unified scan history | All scans (manual + scheduled) in one searchable modal with Manual/Scheduled type badges |
| Auto-cleanup | Clone namespace deleted after scan; stale namespaces cleaned on pod startup |
Scheduled Malware Scanner
| Feature | Detail |
|---|---|
| Daily automated scanning | All eligible policies (those with PVC data) scanned once per day at a configured UTC time |
| Snapshot-first strategy | Uses the latest snapshot RP for speed (seconds). Falls back to latest export if no snapshot exists |
| Sequential with gap | One policy at a time with a configurable inter-policy gap (default 5 min) to limit resource usage |
| Persistent state | Schedule config and run results stored on PVC — survives pod restarts. Scheduler resumes at next daily time after restart |
| Enable/disable toggle | Master switch disables scanning without losing config — useful during maintenance |
| Run Now | Manual trigger from the Scheduled Scan modal, independent of the daily schedule |
| In-app alert banner | Red banner below the header on page load if the latest run found threats. Shows timestamp, dirty policy count and names. Dismissible per run, stored on PVC |
| Email alerts | Gmail SMTP (app password) alert email with full HTML threat table per policy. Test email button to verify config |
| Unified history | Results appear in the Malware Scan History modal alongside manual scans, tagged "Scheduled" |
UI & Advanced Features
| Feature | Detail |
|---|---|
| Veeam-aligned design | Veeam green #00c853 throughout. Flat dark header, ghost buttons, segmented destination tabs, card-based Steps 4/5/6 |
| Diff mode | Mount two export RPs simultaneously — Diff tab shows added (green), deleted (red), modified (amber), unchanged (grey) |
| FLR history detail panels | Click any FLR history row to expand: metadata grid (RP, policy, location profile, duration, mode) and transfer detail table |
| Scan history filters | Filter by policy/RP/PVC text, date picker (all scans on a given day), type (Manual/Scheduled), result (Clean/Dirty) |
| Scan history detail panels | Click any scan row to expand: metadata grid + full threat table with reference links |
| API call visualiser | Resizable panel at page bottom. Syntax-highlighted kubectl commands — green verb, white resource, grey namespace |
| Health dashboard | Version strip, mount headroom progress bar, FLR session count, recovery history donut chart, active sessions table |
| Kubernetes token auth | TokenReview validation, 8-hour HMAC-signed session cookie |
| Dark/light theme | Full theme toggle persisted in localStorage. Veeam logo uses transparent PNG corners — correct on both themes |
| Startup cleanup | Deletes stale flr-ui-* FileRecoverySessions and kasten-malware-scan-* namespaces on pod start |
| Graceful shutdown | Cleans FLR session, SFTP connection and staging area on SIGTERM |
Prerequisites
- OpenShift 4.x with Kasten K10 installed in
kasten-io - Harbor registry at
harbor.apps.openshift2.lab.home(update image refs if different) ocCLI authenticated to the clusterdockeron the build machine- Gmail account with an App Password for scheduled scan email alerts (optional)
File Structure
kasten-flr-ui/
├── Dockerfile # Multi-stage Alpine (Python + kubectl + nginx)
├── README.md
├── backend/
│ ├── main.py # FastAPI app, all routers, startup/shutdown, scheduler init
│ ├── auth.py # K8s token validation, HMAC session cookies
│ ├── flr.py # FileRecoverySession CR lifecycle
│ ├── sftp.py # Paramiko SFTP client via kubectl port-forward
│ ├── transfer.py # kubectl cp, SCP, ZIP transfer engines
│ ├── kasten.py # Kubernetes API client helpers
│ ├── history.py # FLR session history (JSON on PVC)
│ ├── keys.py # ed25519 keypair generation
│ ├── scan_routes.py # On-demand malware scan API + SSE stream
│ ├── yara_routes.py # YARA rules upload/list/delete/settings
│ ├── schedule_routes.py # Scheduled scanner, background loop, email alerts
│ └── requirements.txt
├── frontend/
│ ├── index.html # Main app — 6-step wizard + all modals
│ ├── login.html # Token login page + capability grid
│ ├── style.css # Full Veeam-aligned dark/light theme
│ ├── app.js # Wizard, file browser, health modal, FLR history
│ ├── scan.js # ScanUI — malware scan overlay, unified history + filters
│ ├── schedule.js # ScheduleUI — scheduler modal, alert banner
│ └── veeam-logo.png # Veeam logo, transparent corners, 24KB optimised
├── scanner-image/
│ ├── Dockerfile # Alpine + YARA + ClamAV scanner image
│ ├── scan.sh # freshclam + YARA + ClamAV, structured output
│ └── rules/
│ └── kasten-starter.yar # Starter YARA rules (37 rules, 9 categories)
├── nginx/
│ └── nginx.conf # Proxy, SSE support, large download config
└── k8s/
├── namespace.yaml
├── serviceaccount.yaml
├── clusterrole.yaml
├── clusterrolebinding.yaml
├── scc.yaml
├── pvc.yaml # 1Gi PVC for all persistent state
├── deployment.yaml # Recreate strategy, emptyDir staging (10Gi)
├── service.yaml
└── route.yaml
Initial Deployment
1. Build and push both images
cd kasten-flr-ui
# Main FLR UI image
docker build -t harbor.apps.openshift2.lab.home/kasten-flr-ui/kasten-flr-ui:latest .
docker push harbor.apps.openshift2.lab.home/kasten-flr-ui/kasten-flr-ui:latest
# Malware scanner image
cd scanner-image/
docker build -t harbor.apps.openshift2.lab.home/kasten-flr-ui/malware-scanner:latest .
docker push harbor.apps.openshift2.lab.home/kasten-flr-ui/malware-scanner:latest
cd ..
2. Apply Kubernetes manifests
oc apply -f k8s/namespace.yaml
oc apply -f k8s/serviceaccount.yaml
oc apply -f k8s/clusterrole.yaml
oc apply -f k8s/clusterrolebinding.yaml
oc apply -f k8s/scc.yaml
oc apply -f k8s/pvc.yaml
oc apply -f k8s/deployment.yaml
oc apply -f k8s/service.yaml
oc apply -f k8s/route.yaml
3. Verify
oc rollout status deployment/kasten-flr-ui -n kasten-flr-ui
oc get route kasten-flr-ui -n kasten-flr-ui
oc exec deployment/kasten-flr-ui -n kasten-flr-ui -- df -h /data
Update & Redeploy
# Full rebuild + push + restart + archive
docker build -t harbor.apps.openshift2.lab.home/kasten-flr-ui/kasten-flr-ui:latest . && \
docker push harbor.apps.openshift2.lab.home/kasten-flr-ui/kasten-flr-ui:latest && \
oc rollout restart deployment/kasten-flr-ui -n kasten-flr-ui && \
tar -czf ~/kasten-flr-ui-$(date +%Y%m%d-%H%M%S).tar.gz -C ~ kasten-flr-ui/ && \
echo "✓ Done"
Hot-copy frontend files without a full rebuild:
POD=$(oc get pod -n kasten-flr-ui -l app=kasten-flr-ui -o jsonpath='{.items[0].metadata.name}')
oc cp frontend/app.js kasten-flr-ui/$POD:/usr/share/nginx/html/app.js
oc cp frontend/scan.js kasten-flr-ui/$POD:/usr/share/nginx/html/scan.js
oc cp frontend/schedule.js kasten-flr-ui/$POD:/usr/share/nginx/html/schedule.js
oc cp frontend/style.css kasten-flr-ui/$POD:/usr/share/nginx/html/style.css
oc cp frontend/index.html kasten-flr-ui/$POD:/usr/share/nginx/html/index.html
oc cp frontend/login.html kasten-flr-ui/$POD:/usr/share/nginx/html/login.html
Rebuild scanner image only:
cd scanner-image/
docker build -t harbor.apps.openshift2.lab.home/kasten-flr-ui/malware-scanner:latest .
docker push harbor.apps.openshift2.lab.home/kasten-flr-ui/malware-scanner:latest
Recovery Workflow
Step 1 — Select Policy
Real-time search/filter by name or namespace.
Shimmer skeleton while loading. Excludes VM and no-PVC policies.
Step 2 — Select Restore Point
Snapshot and export RPs with type badges.
Paired RPs (same backup run) grouped side-by-side.
"Scan for Malware" to verify the RP before recovery.
Diff dropdown for export RPs (select a second RP to compare).
Step 3 — Start FLR Session
Confirmation card: policy avatar, RP name, PVC pills, headroom warning.
Creates FileRecoverySession CR. Polls until Ready. Connects SFTP.
Step 4 — Browse & Select Files
Left: SFTP browser with 60+ file-type SVG icons.
Hover any file for tooltip: size, modified date, full path.
Right: selection basket. Drag-and-drop or checkbox.
Diff tab available in diff mode.
Step 5 — Choose Destination
Segmented tab control: Pod/PVC · SSH Host · ZIP download.
Pod mode: in-browser filesystem navigator to pick destination path.
Step 6 — Transfer
Progress card + colour-coded SSE log.
Spring-animated green success banner on completion.
Terminate → namespace cleanup → return to Step 1.
Malware Scanning
On-demand scan
Click Scan for Malware on any restore point in Step 2. A full-screen three-phase modal opens:
Phase 1 — Restore: Creates an isolated kasten-malware-scan-<id> namespace. Kasten RestoreAction clones the restore point PVCs. Workloads are scaled to 0 to release PVC mounts. Progress bar streams in real time.
Phase 2 — Scan: freshclam updates ClamAV signatures (60s timeout, falls back to baked-in sigs if offline). YARA scans files under the configured size threshold. ClamAV scans all files. Threats appear in real time as they are detected. Per-PVC status pills show progress.
Phase 3 — Results: Animated CLEAN (green) or DIRTY (red) banner. Threat table shows scanner, rule name and file path. Known families link to MITRE ATT&CK or CISA advisories. Clone namespace deleted (or retained if you choose to keep it for investigation).
Recommended workflow
Scan the snapshot (local, seconds) → if clean, proceed with FLR using the export RP.
Scheduled Malware Scanner
Configuration
Open the Scheduled Scan modal from the header. Settings are persisted to /data/scan-schedule.json.
| Setting | Default | Description |
|---|---|---|
| Enabled | Off | Master on/off switch |
| Run time (UTC) | 02:00 | Daily execution time |
| Gap between policies | 5 min | Pause between each policy scan |
| Gmail address | — | SMTP sender (your@gmail.com) |
| Gmail app password | — | 16-character Google app password |
| Alert recipients | — | Comma-separated email addresses |
Use Send Test Email to verify SMTP config before enabling alerts.
Execution flow
- Background asyncio task wakes every 60 seconds
- Reads config — if enabled and past scheduled time, starts a run
- Fetches all policies with PVC data (same filter as Step 1)
- For each policy: gets the latest snapshot RP; falls back to export if none
- Creates isolated scan namespace, restores PVCs, runs YARA + ClamAV
- Writes result to
/data/scan-history.jsonwithscan_type: "scheduled" - Waits the configured gap, then moves to the next policy
- After all policies: if any DIRTY → sends alert email + sets in-app banner
- Computes and saves next daily run time
In-app alert banner
A red banner appears below the header on the next page load after a dirty run. It shows the run timestamp, the number of dirty policies and their names. Clicking View Details opens the unified Malware Scan History modal pre-filtered to that run's results. The banner is dismissible per run — dismissal is written to the PVC and persists across refreshes.
Alert email
An HTML email is sent via Gmail SMTP if any policy scan finds threats. It includes a table of affected policies, namespaces, threat counts and per-threat detail (scanner, rule, file path).
YARA Rules Management
Click YARA Rules in the header.
- Upload
.yaror.yarafiles via drag-and-drop or file picker - Stored on the persistent PVC at
/data/yara-rules/ - Injected into each scanner Job via a Kubernetes ConfigMap at scan time
- Falls back to image-baked
kasten-starter.yarif no user rules are present - Preview rule content and delete individual files from the modal
- Max file size — files larger than this threshold are skipped by YARA (default 1MB, configurable). Stored in
/data/yara-settings.json
Starter rules (kasten-starter.yar) — 37 rules, 9 categories
| Category | Rules |
|---|---|
| Test | EICAR test file |
| Ransomware | WannaCry, LockBit, REvil/Sodinokibi, Conti, BlackCat/ALPHV, Phobos |
| Web shells | PHP shell, ASPX shell, JSP shell, China Chopper |
| Credential stealers | RedLine, Raccoon, AgentTesla |
| RATs | AsyncRAT, QuasarRAT |
| Offensive tools | Cobalt Strike, Mimikatz, Metasploit/Meterpreter |
| Miners | XMRig cryptocurrency miner |
| Loaders | PowerShell downloader, Linux reverse shell |
| Kubernetes-specific | Secret exfiltration, container escape, K8s API abuse |
Snapshot vs Export Restore Points
| Type | Badge | FLR | Diff mode | Malware scan | Restore speed |
|---|---|---|---|---|---|
| Snapshot | Green | ✗ | ✗ | ✓ | Seconds (local) |
| Export | Blue | ✓ | ✓ | ✓ | Minutes (S3/object store) |
Snapshot RPs are stored locally on the cluster. Export RPs are stored in an external location profile (S3, NFS, etc.) and must be pulled down before the restore point can be used — hence the speed difference.
Authentication
- Browse to the app URL — unauthenticated users are redirected to
/login - Obtain your token:
oc whoami -t - Paste the token and click Authenticate
- The backend validates the token via the Kubernetes
TokenReviewAPI - A signed session cookie is issued (8-hour expiry, HMAC-signed with
itsdangerous) - Any 401 from the backend automatically redirects to
/login
Session History
PVC data layout
| Path | Content | Max records |
|---|---|---|
/data/flr-history.json |
FLR session records | 500 |
/data/scan-history.json |
All scan records (manual + scheduled, scan_type field) |
500 |
/data/scan-schedule.json |
Scheduler config and last/next run state | — |
/data/scheduled-scan-results.json |
Per-run summary records for the scheduler | 200 |
/data/yara-rules/ |
User YARA rule files | — |
/data/yara-settings.json |
YARA max file size setting | — |
All writes are atomic: write to .tmp then os.replace(). Oldest records are pruned automatically when limits are reached.
FLR History modal
Click FLR History in the header. Click any row to expand a detail panel showing:
- Restore point name, policy, location profile, session start, duration, mode (Standard/Diff)
- Transfer table: destination type, host/pod, item count, outcome
Malware Scan History modal
Click Malware Scan History in the header (or View Scan History in the Scheduled Scan modal).
Filter bar controls:
- Text search — filters by policy name, restore point name or PVC name
- Date picker — shows only scans from a selected day (useful for reviewing all scheduled runs on a given date)
- Type — All / Manual / Scheduled
- Result — All / Clean / Dirty
Click any row to expand: scanner versions (YARA/ClamAV), duration, restore point date, and a full threat table with MITRE/CISA reference links.
Health Dashboard
Click Health in the header. Auto-refreshes every 30 seconds while open.
| Component | Description |
|---|---|
| Version strip | Kasten K10 version · cluster version · platform (OpenShift / Kubernetes) |
| Mount Headroom | Progress bar — mounts in use vs frs.maxMountsPerNamespace limit. Red <25%, amber <50%, green otherwise |
| FLR Sessions | Cluster-wide count of FileRecoverySessions; active session indicator |
| Recovery History | Canvas donut chart — completed (green) / terminated (amber) / failed (red) |
| Active sessions table | All cluster-wide FLR sessions with namespace pills, state badge and expiry time |
Technical Notes
Scheduler persistence — Config and run results stored on PVC. Pod restarts resume at the next configured daily time. Any in-progress scan interrupted by a restart will have its clone namespace cleaned up at next startup.
One FLR session at a time — The backend tracks a single active session. A second start attempt returns HTTP 409.
ClamAV signature updates — freshclam runs with a 60-second timeout at the start of every scan (both on-demand and scheduled). Falls back gracefully to image-baked signatures if the network is unavailable.
Staging area — Selected files are downloaded from SFTP to /tmp/flr-stage (emptyDir, 10Gi limit) before onward transfer. Adjust the emptyDir.sizeLimit in k8s/deployment.yaml for large recoveries.
Session expiry — Kasten default is 30 minutes, configurable via Helm frs.sessionExpiryTimeInMinutes. The live countdown is shown in the header session badge. Click the badge while a session is active to get a terminate dropdown.
Deployment strategy — strategy: Recreate because the PVC is ReadWriteOnce — only one pod can mount it at a time. This means a brief downtime during rolling deploys.
NetworkPolicy bypass — Kasten's FLR NetworkPolicy restricts SFTP ingress to the application namespace. kubectl port-forward tunnels through the K8s API server, which is exempt from NetworkPolicy, providing a clean bypass.
API call visualiser — The panel at the bottom of the page is resizable by dragging its top edge. Height preference is saved to localStorage.
Environment Variables
| Variable | Default | Description |
|---|---|---|
DATA_DIR |
/data |
Persistent PVC mount point for all state |
SCANNER_IMAGE |
harbor.apps.openshift2.lab.home/kasten-flr-ui/malware-scanner:latest |
Container image used for malware scan Jobs |
RBAC Summary
The kasten-flr-ui ClusterRole grants the service account the following permissions:
| Resource | API Group | Verbs |
|---|---|---|
restorepoints, restorepoints/details |
apps.kio.kasten.io |
get, list, watch |
restorepointcontents, restorepointcontents/details |
apps.kio.kasten.io |
get, list |
policies |
config.kio.kasten.io |
get, list |
filerecoverysessions |
datamover.kio.kasten.io |
get, list, create, delete, watch |
restoreactions |
actions.kio.kasten.io |
get, list, create, watch, delete |
namespaces |
core | get, list, create, delete |
pods |
core | get, list, watch |
pods/exec |
core | create, get |
pods/portforward |
core | create, get |
pods/log |
core | get, list, watch |
persistentvolumeclaims |
core | get, list, watch |
services |
core | get, list |
configmaps |
core | get, list, create |
deployments, statefulsets, replicasets |
apps |
get, list, watch, patch |
jobs |
batch |
get, list, create, watch, delete |
clusterversions |
config.openshift.io |
get, list |
clusterserviceversions |
operators.coreos.com |
get, list |
Kasten FLR API Reference
- FileRecoverySession API: https://docs.kasten.io/latest/api/filerecoverysessions
- RestorePoint API: https://docs.kasten.io/latest/api/restorepoints
- RestoreAction API: https://docs.kasten.io/latest/api/restoreactions
- Policy API: https://docs.kasten.io/latest/api/policies