feat: implement modular security services with CrowdSec and WAF integration
This commit is contained in:
@@ -93,6 +93,8 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.9.1 \
|
||||
--with github.com/greenpau/caddy-security \
|
||||
--with github.com/corazawaf/coraza-caddy/v2 \
|
||||
--with github.com/hslatman/caddy-crowdsec-bouncer \
|
||||
--replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \
|
||||
--replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \
|
||||
--output /usr/bin/caddy
|
||||
|
||||
113
SECURITY_IMPLEMENTATION_PLAN.md
Normal file
113
SECURITY_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Security Services Implementation Plan
|
||||
|
||||
## Overview
|
||||
This document outlines the plan to implement a modular Security Dashboard in CaddyProxyManager+ (CPM+). The goal is to provide optional, high-value security integrations (CrowdSec, WAF, ACLs, Rate Limiting) while keeping the core Docker image lightweight.
|
||||
|
||||
## Core Philosophy
|
||||
1. **Optionality**: All security services are disabled by default.
|
||||
2. **Environment Driven**: Activation is controlled via `CPM_SECURITY_*` environment variables.
|
||||
3. **Minimal Footprint**:
|
||||
* Lightweight Caddy modules (WAF, Bouncers) are compiled into the binary (negligible size impact).
|
||||
* Heavy standalone agents (e.g., CrowdSec Agent) are only installed at runtime if explicitly enabled in "Local" mode.
|
||||
4. **Unified Dashboard**: A single pane of glass in the UI to view status and configuration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment Variables
|
||||
We will introduce a new set of environment variables to control these services.
|
||||
|
||||
| Variable | Values | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `CPM_SECURITY_CROWDSEC_MODE` | `disabled` (default), `local`, `external` | `local` installs agent inside container; `external` uses remote agent. |
|
||||
| `CPM_SECURITY_CROWDSEC_API_URL` | URL (e.g., `http://crowdsec:8080`) | Required if mode is `external`. |
|
||||
| `CPM_SECURITY_CROWDSEC_API_KEY` | String | Required if mode is `external`. |
|
||||
| `CPM_SECURITY_WAF_MODE` | `disabled` (default), `enabled` | Enables Coraza WAF with OWASP Core Rule Set (CRS). |
|
||||
| `CPM_SECURITY_RATELIMIT_ENABLED` | `true`, `false` (default) | Enables global rate limiting controls. |
|
||||
| `CPM_SECURITY_ACL_ENABLED` | `true`, `false` (default) | Enables IP-based Access Control Lists. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend Implementation
|
||||
|
||||
### A. Dockerfile Updates
|
||||
We need to compile the necessary Caddy modules into our binary. This adds minimal size overhead but enables the features natively.
|
||||
* **Action**: Update `Dockerfile` `caddy-builder` stage to include:
|
||||
* `github.com/corazawaf/coraza-caddy/v2` (WAF)
|
||||
* `github.com/hslatman/caddy-crowdsec-bouncer` (CrowdSec Bouncer)
|
||||
|
||||
### B. Configuration Management (`internal/config`)
|
||||
* **Action**: Update `Config` struct to parse `CPM_SECURITY_*` variables.
|
||||
* **Action**: Create `SecurityConfig` struct to hold these values.
|
||||
|
||||
### C. Runtime Installation (`docker-entrypoint.sh`)
|
||||
To satisfy the "install locally" requirement for CrowdSec without bloating the image:
|
||||
* **Action**: Modify `docker-entrypoint.sh` to check `CPM_SECURITY_CROWDSEC_MODE`.
|
||||
* **Logic**: If `local`, execute `apk add --no-cache crowdsec` (and dependencies) before starting the app. This keeps the base image small for users who don't use it.
|
||||
|
||||
### D. API Endpoints (`internal/api`)
|
||||
* **New Endpoint**: `GET /api/v1/security/status`
|
||||
* Returns the enabled/disabled state of each service.
|
||||
* Returns basic metrics if available (e.g., "WAF: Active", "CrowdSec: Connected").
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend Implementation
|
||||
|
||||
### A. Navigation
|
||||
* **Action**: Add "Security" item to the Sidebar in `Layout.tsx`.
|
||||
|
||||
### B. Security Dashboard (`src/pages/Security.tsx`)
|
||||
* **Layout**: Grid of cards representing each service.
|
||||
* **Empty State**: If all services are disabled, show a clean "Security Not Enabled" state with a link to the GitHub Pages documentation on how to enable them.
|
||||
|
||||
### C. Service Cards
|
||||
1. **CrowdSec Card**:
|
||||
* **Status**: Active (Local/External) / Disabled.
|
||||
* **Content**: If Local, show basic stats (last push, alerts). If External, show connection status.
|
||||
* **Action**: Link to CrowdSec Console or Dashboard.
|
||||
2. **WAF Card**:
|
||||
* **Status**: Active / Disabled.
|
||||
* **Content**: "OWASP CRS Loaded".
|
||||
3. **Access Control Lists (ACL)**:
|
||||
* **Status**: Active / Disabled.
|
||||
* **Action**: "Manage Blocklists" (opens modal/page to edit IP lists).
|
||||
4. **Rate Limiting**:
|
||||
* **Status**: Active / Disabled.
|
||||
* **Action**: "Configure Limits" (opens modal to set global requests/second).
|
||||
|
||||
---
|
||||
|
||||
## 4. Service-Specific Logic
|
||||
|
||||
### CrowdSec
|
||||
* **Local**:
|
||||
* Installs CrowdSec agent via `apk`.
|
||||
* Generates `acquis.yaml` to read Caddy logs.
|
||||
* Configures Caddy bouncer to talk to `localhost:8080`.
|
||||
* **External**:
|
||||
* Configures Caddy bouncer to talk to `CPM_SECURITY_CROWDSEC_API_URL`.
|
||||
|
||||
### WAF (Coraza)
|
||||
* **Implementation**:
|
||||
* When enabled, inject `coraza_waf` directive into the global Caddyfile or per-host.
|
||||
* Use default OWASP Core Rule Set (CRS).
|
||||
|
||||
### IP ACLs
|
||||
* **Implementation**:
|
||||
* Create a snippet `(ip_filter)` in Caddyfile.
|
||||
* Use `@matcher` with `remote_ip` to block/allow IPs.
|
||||
* UI allows adding CIDR ranges to this list.
|
||||
|
||||
### Rate Limiting
|
||||
* **Implementation**:
|
||||
* Use `rate_limit` directive.
|
||||
* Allow user to define "zones" (e.g., API, Static) in the UI.
|
||||
|
||||
---
|
||||
|
||||
## 5. Documentation
|
||||
* **New Doc**: `docs/security.md`
|
||||
* **Content**:
|
||||
* Explanation of each service.
|
||||
* How to configure Env Vars.
|
||||
* Trade-offs of "Local" CrowdSec (startup time vs convenience).
|
||||
42
backend/internal/api/handlers/security_handler.go
Normal file
42
backend/internal/api/handlers/security_handler.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
)
|
||||
|
||||
// SecurityHandler handles security-related API requests.
|
||||
type SecurityHandler struct {
|
||||
cfg config.SecurityConfig
|
||||
}
|
||||
|
||||
// NewSecurityHandler creates a new SecurityHandler.
|
||||
func NewSecurityHandler(cfg config.SecurityConfig) *SecurityHandler {
|
||||
return &SecurityHandler{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStatus returns the current status of all security services.
|
||||
func (h *SecurityHandler) GetStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"crowdsec": gin.H{
|
||||
"mode": h.cfg.CrowdSecMode,
|
||||
"api_url": h.cfg.CrowdSecAPIURL,
|
||||
"enabled": h.cfg.CrowdSecMode != "disabled",
|
||||
},
|
||||
"waf": gin.H{
|
||||
"mode": h.cfg.WAFMode,
|
||||
"enabled": h.cfg.WAFMode == "enabled",
|
||||
},
|
||||
"rate_limit": gin.H{
|
||||
"enabled": h.cfg.RateLimitEnabled,
|
||||
},
|
||||
"acl": gin.H{
|
||||
"enabled": h.cfg.ACLEnabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
105
backend/internal/api/handlers/security_handler_test.go
Normal file
105
backend/internal/api/handlers/security_handler_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
)
|
||||
|
||||
func TestSecurityHandler_GetStatus(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg config.SecurityConfig
|
||||
expectedStatus int
|
||||
expectedBody map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "All Disabled",
|
||||
cfg: config.SecurityConfig{
|
||||
CrowdSecMode: "disabled",
|
||||
WAFMode: "disabled",
|
||||
RateLimitEnabled: false,
|
||||
ACLEnabled: false,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: map[string]interface{}{
|
||||
"crowdsec": map[string]interface{}{
|
||||
"mode": "disabled",
|
||||
"api_url": "",
|
||||
"enabled": false,
|
||||
},
|
||||
"waf": map[string]interface{}{
|
||||
"mode": "disabled",
|
||||
"enabled": false,
|
||||
},
|
||||
"rate_limit": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
"acl": map[string]interface{}{
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "All Enabled",
|
||||
cfg: config.SecurityConfig{
|
||||
CrowdSecMode: "local",
|
||||
WAFMode: "enabled",
|
||||
RateLimitEnabled: true,
|
||||
ACLEnabled: true,
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: map[string]interface{}{
|
||||
"crowdsec": map[string]interface{}{
|
||||
"mode": "local",
|
||||
"api_url": "",
|
||||
"enabled": true,
|
||||
},
|
||||
"waf": map[string]interface{}{
|
||||
"mode": "enabled",
|
||||
"enabled": true,
|
||||
},
|
||||
"rate_limit": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
"acl": map[string]interface{}{
|
||||
"enabled": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
handler := NewSecurityHandler(tt.cfg)
|
||||
router := gin.New()
|
||||
router.GET("/security/status", handler.GetStatus)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/security/status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Helper to convert map[string]interface{} to JSON and back to normalize types
|
||||
// (e.g. int vs float64)
|
||||
expectedJSON, _ := json.Marshal(tt.expectedBody)
|
||||
var expectedNormalized map[string]interface{}
|
||||
json.Unmarshal(expectedJSON, &expectedNormalized)
|
||||
|
||||
assert.Equal(t, expectedNormalized, response)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,10 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error {
|
||||
go uptimeService.CheckAll()
|
||||
c.JSON(200, gin.H{"message": "Uptime check started"})
|
||||
})
|
||||
|
||||
// Security Status
|
||||
securityHandler := handlers.NewSecurityHandler(cfg.Security)
|
||||
protected.GET("/security/status", securityHandler.GetStatus)
|
||||
}
|
||||
|
||||
// Caddy Manager
|
||||
|
||||
@@ -19,6 +19,17 @@ type Config struct {
|
||||
ImportDir string
|
||||
JWTSecret string
|
||||
ACMEStaging bool
|
||||
Security SecurityConfig
|
||||
}
|
||||
|
||||
// SecurityConfig holds configuration for optional security services.
|
||||
type SecurityConfig struct {
|
||||
CrowdSecMode string
|
||||
CrowdSecAPIURL string
|
||||
CrowdSecAPIKey string
|
||||
WAFMode string
|
||||
RateLimitEnabled bool
|
||||
ACLEnabled bool
|
||||
}
|
||||
|
||||
// Load reads env vars and falls back to defaults so the server can boot with zero configuration.
|
||||
@@ -35,6 +46,14 @@ func Load() (Config, error) {
|
||||
ImportDir: getEnv("CPM_IMPORT_DIR", filepath.Join("data", "imports")),
|
||||
JWTSecret: getEnv("CPM_JWT_SECRET", "change-me-in-production"),
|
||||
ACMEStaging: getEnv("CPM_ACME_STAGING", "") == "true",
|
||||
Security: SecurityConfig{
|
||||
CrowdSecMode: getEnv("CPM_SECURITY_CROWDSEC_MODE", "disabled"),
|
||||
CrowdSecAPIURL: getEnv("CPM_SECURITY_CROWDSEC_API_URL", ""),
|
||||
CrowdSecAPIKey: getEnv("CPM_SECURITY_CROWDSEC_API_KEY", ""),
|
||||
WAFMode: getEnv("CPM_SECURITY_WAF_MODE", "disabled"),
|
||||
RateLimitEnabled: getEnv("CPM_SECURITY_RATELIMIT_ENABLED", "false") == "true",
|
||||
ACLEnabled: getEnv("CPM_SECURITY_ACL_ENABLED", "false") == "true",
|
||||
},
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(cfg.DatabasePath), 0o755); err != nil {
|
||||
|
||||
@@ -19,6 +19,13 @@ services:
|
||||
- CPM_FRONTEND_DIR=/app/frontend/dist
|
||||
- CPM_CADDY_ADMIN_API=http://localhost:2019
|
||||
- CPM_CADDY_CONFIG_DIR=/app/data/caddy
|
||||
# Security Services (Optional)
|
||||
#- CPM_SECURITY_CROWDSEC_MODE=disabled
|
||||
#- CPM_SECURITY_CROWDSEC_API_URL=
|
||||
#- CPM_SECURITY_CROWDSEC_API_KEY=
|
||||
#- CPM_SECURITY_WAF_MODE=disabled
|
||||
#- CPM_SECURITY_RATELIMIT_ENABLED=false
|
||||
#- CPM_SECURITY_ACL_ENABLED=false
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
|
||||
# Mount your existing Caddyfile for automatic import (optional)
|
||||
|
||||
@@ -24,6 +24,13 @@ services:
|
||||
- CPM_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CPM_IMPORT_DIR=/app/data/imports
|
||||
- CPM_ACME_STAGING=false
|
||||
# Security Services (Optional)
|
||||
- CPM_SECURITY_CROWDSEC_MODE=disabled
|
||||
- CPM_SECURITY_CROWDSEC_API_URL=
|
||||
- CPM_SECURITY_CROWDSEC_API_KEY=
|
||||
- CPM_SECURITY_WAF_MODE=disabled
|
||||
- CPM_SECURITY_RATELIMIT_ENABLED=false
|
||||
- CPM_SECURITY_ACL_ENABLED=false
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
cap_add:
|
||||
|
||||
@@ -21,6 +21,13 @@ services:
|
||||
- CPM_CADDY_BINARY=caddy
|
||||
- CPM_IMPORT_CADDYFILE=/import/Caddyfile
|
||||
- CPM_IMPORT_DIR=/app/data/imports
|
||||
# Security Services (Optional)
|
||||
#- CPM_SECURITY_CROWDSEC_MODE=disabled # disabled, local, external
|
||||
#- CPM_SECURITY_CROWDSEC_API_URL= # Required if mode is external
|
||||
#- CPM_SECURITY_CROWDSEC_API_KEY= # Required if mode is external
|
||||
#- CPM_SECURITY_WAF_MODE=disabled # disabled, enabled
|
||||
#- CPM_SECURITY_RATELIMIT_ENABLED=false
|
||||
#- CPM_SECURITY_ACL_ENABLED=false
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
|
||||
@@ -6,6 +6,29 @@ set -e
|
||||
|
||||
echo "Starting CaddyProxyManager+ with integrated Caddy..."
|
||||
|
||||
# Optional: Install and start CrowdSec (Local Mode)
|
||||
CROWDSEC_PID=""
|
||||
if [ "$CPM_SECURITY_CROWDSEC_MODE" = "local" ]; then
|
||||
echo "CrowdSec Local Mode enabled. Installing CrowdSec agent..."
|
||||
# Install crowdsec from community repository if needed
|
||||
apk add --no-cache crowdsec --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community || \
|
||||
apk add --no-cache crowdsec || \
|
||||
echo "Failed to install crowdsec. Check repositories."
|
||||
|
||||
if command -v crowdsec >/dev/null; then
|
||||
echo "Starting CrowdSec agent..."
|
||||
# Ensure configuration exists or is generated (basic check)
|
||||
if [ ! -d "/etc/crowdsec" ]; then
|
||||
echo "Warning: /etc/crowdsec not found. CrowdSec might fail to start."
|
||||
fi
|
||||
crowdsec &
|
||||
CROWDSEC_PID=$!
|
||||
echo "CrowdSec started (PID: $CROWDSEC_PID)"
|
||||
else
|
||||
echo "CrowdSec binary not found after installation attempt."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start Caddy in the background with initial empty config
|
||||
echo '{"apps":{}}' > /config/caddy.json
|
||||
# Use JSON config directly; no adapter needed
|
||||
@@ -40,6 +63,11 @@ shutdown() {
|
||||
echo "Shutting down..."
|
||||
kill -TERM "$APP_PID" 2>/dev/null || true
|
||||
kill -TERM "$CADDY_PID" 2>/dev/null || true
|
||||
if [ -n "$CROWDSEC_PID" ]; then
|
||||
echo "Stopping CrowdSec..."
|
||||
kill -TERM "$CROWDSEC_PID" 2>/dev/null || true
|
||||
wait "$CROWDSEC_PID" 2>/dev/null || true
|
||||
fi
|
||||
wait "$APP_PID" 2>/dev/null || true
|
||||
wait "$CADDY_PID" 2>/dev/null || true
|
||||
exit 0
|
||||
|
||||
@@ -20,6 +20,7 @@ const Backups = lazy(() => import('./pages/Backups'))
|
||||
const Tasks = lazy(() => import('./pages/Tasks'))
|
||||
const Logs = lazy(() => import('./pages/Logs'))
|
||||
const Domains = lazy(() => import('./pages/Domains'))
|
||||
const Security = lazy(() => import('./pages/Security'))
|
||||
const Uptime = lazy(() => import('./pages/Uptime'))
|
||||
const Notifications = lazy(() => import('./pages/Notifications'))
|
||||
const Login = lazy(() => import('./pages/Login'))
|
||||
@@ -47,6 +48,7 @@ export default function App() {
|
||||
<Route path="remote-servers" element={<RemoteServers />} />
|
||||
<Route path="domains" element={<Domains />} />
|
||||
<Route path="certificates" element={<Certificates />} />
|
||||
<Route path="security" element={<Security />} />
|
||||
<Route path="uptime" element={<Uptime />} />
|
||||
<Route path="notifications" element={<Notifications />} />
|
||||
<Route path="import" element={<ImportCaddy />} />
|
||||
|
||||
24
frontend/src/api/security.ts
Normal file
24
frontend/src/api/security.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import client from './client'
|
||||
|
||||
export interface SecurityStatus {
|
||||
crowdsec: {
|
||||
mode: 'disabled' | 'local' | 'external'
|
||||
api_url: string
|
||||
enabled: boolean
|
||||
}
|
||||
waf: {
|
||||
mode: 'disabled' | 'enabled'
|
||||
enabled: boolean
|
||||
}
|
||||
rate_limit: {
|
||||
enabled: boolean
|
||||
}
|
||||
acl: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const getSecurityStatus = async (): Promise<SecurityStatus> => {
|
||||
const response = await client.get<SecurityStatus>('/security/status')
|
||||
return response.data
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{ name: 'Remote Servers', path: '/remote-servers', icon: '🖥️' },
|
||||
{ name: 'Domains', path: '/domains', icon: '🌍' },
|
||||
{ name: 'Certificates', path: '/certificates', icon: '🔒' },
|
||||
{ name: 'Security', path: '/security', icon: '🛡️' },
|
||||
{ name: 'Uptime', path: '/uptime', icon: '📈' },
|
||||
{ name: 'Notifications', path: '/notifications', icon: '🔔' },
|
||||
{ name: 'Import Caddyfile', path: '/import', icon: '📥' },
|
||||
@@ -91,15 +92,15 @@ export default function Layout({ children }: LayoutProps) {
|
||||
${mobileSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
${isCollapsed ? 'w-20' : 'w-64'}
|
||||
`}>
|
||||
<div className={`h-16 flex items-center justify-center border-b border-gray-200 dark:border-gray-800`}>
|
||||
<div className={`h-20 flex items-center justify-center border-b border-gray-200 dark:border-gray-800`}>
|
||||
{isCollapsed ? (
|
||||
<img src="/logo.png" alt="CPM+" className="h-10 w-10" />
|
||||
<img src="/logo.png" alt="CPM+" className="h-12 w-10" />
|
||||
) : (
|
||||
<img src="/banner.png" alt="CPM+" className="h-12 w-auto" />
|
||||
<img src="/banner.png" alt="CPM+" className="h-16 w-auto" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 px-4 mt-16 lg:mt-0">
|
||||
<div className="flex flex-col flex-1 px-4 mt-16 lg:mt-6">
|
||||
<nav className="flex-1 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
if (item.children) {
|
||||
@@ -246,7 +247,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{/* Main Content */}
|
||||
<main className={`flex-1 min-w-0 overflow-auto pt-16 lg:pt-0 flex flex-col transition-all duration-200 ${isCollapsed ? 'lg:ml-20' : 'lg:ml-64'}`}>
|
||||
{/* Desktop Header */}
|
||||
<header className="hidden lg:flex items-center justify-between px-8 py-4 bg-white dark:bg-dark-sidebar border-b border-gray-200 dark:border-gray-800 relative">
|
||||
<header className="hidden lg:flex items-center justify-between px-8 h-20 bg-white dark:bg-dark-sidebar border-b border-gray-200 dark:border-gray-800 relative">
|
||||
<div className="w-1/3 flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
|
||||
160
frontend/src/pages/Security.tsx
Normal file
160
frontend/src/pages/Security.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink } from 'lucide-react'
|
||||
import { getSecurityStatus } from '../api/security'
|
||||
import { Card } from '../components/ui/Card'
|
||||
import { Button } from '../components/ui/Button'
|
||||
|
||||
export default function Security() {
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ['security-status'],
|
||||
queryFn: getSecurityStatus,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="p-8 text-center">Loading security status...</div>
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return <div className="p-8 text-center text-red-500">Failed to load security status</div>
|
||||
}
|
||||
|
||||
const allDisabled = !status.crowdsec.enabled && !status.waf.enabled && !status.rate_limit.enabled && !status.acl.enabled
|
||||
|
||||
if (allDisabled) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center space-y-6">
|
||||
<div className="bg-gray-100 dark:bg-gray-800 p-6 rounded-full">
|
||||
<Shield className="w-16 h-16 text-gray-400" />
|
||||
</div>
|
||||
<div className="max-w-md space-y-2">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">Security Services Not Enabled</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
CaddyProxyManager+ supports advanced security features like CrowdSec, WAF, ACLs, and Rate Limiting.
|
||||
These are optional and can be enabled via environment variables.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => window.open('https://wikid82.github.io/CaddyProxyManagerPlus/security', '_blank')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
View Implementation Guide
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<ShieldCheck className="w-8 h-8 text-green-500" />
|
||||
Security Dashboard
|
||||
</h1>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => window.open('https://wikid82.github.io/CaddyProxyManagerPlus/security', '_blank')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* CrowdSec */}
|
||||
<Card className={status.crowdsec.enabled ? 'border-green-200 dark:border-green-900' : ''}>
|
||||
<div className="flex flex-row items-center justify-between pb-2">
|
||||
<h3 className="text-sm font-medium text-white">CrowdSec</h3>
|
||||
<ShieldAlert className={`w-4 h-4 ${status.crowdsec.enabled ? 'text-green-500' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-1 text-white">
|
||||
{status.crowdsec.enabled ? 'Active' : 'Disabled'}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{status.crowdsec.enabled
|
||||
? `Mode: ${status.crowdsec.mode}`
|
||||
: 'Intrusion Prevention System'}
|
||||
</p>
|
||||
{status.crowdsec.enabled && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => window.open(status.crowdsec.mode === 'external' ? status.crowdsec.api_url : 'http://localhost:8080', '_blank')}
|
||||
>
|
||||
Open Console
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* WAF */}
|
||||
<Card className={status.waf.enabled ? 'border-green-200 dark:border-green-900' : ''}>
|
||||
<div className="flex flex-row items-center justify-between pb-2">
|
||||
<h3 className="text-sm font-medium text-white">WAF (Coraza)</h3>
|
||||
<Shield className={`w-4 h-4 ${status.waf.enabled ? 'text-green-500' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-1 text-white">
|
||||
{status.waf.enabled ? 'Active' : 'Disabled'}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
OWASP Core Rule Set
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ACL */}
|
||||
<Card className={status.acl.enabled ? 'border-green-200 dark:border-green-900' : ''}>
|
||||
<div className="flex flex-row items-center justify-between pb-2">
|
||||
<h3 className="text-sm font-medium text-white">Access Control</h3>
|
||||
<Lock className={`w-4 h-4 ${status.acl.enabled ? 'text-green-500' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-1 text-white">
|
||||
{status.acl.enabled ? 'Active' : 'Disabled'}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
IP-based Allow/Deny Lists
|
||||
</p>
|
||||
{status.acl.enabled && (
|
||||
<div className="mt-4">
|
||||
<Button variant="secondary" size="sm" className="w-full">
|
||||
Manage Lists
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Rate Limiting */}
|
||||
<Card className={status.rate_limit.enabled ? 'border-green-200 dark:border-green-900' : ''}>
|
||||
<div className="flex flex-row items-center justify-between pb-2">
|
||||
<h3 className="text-sm font-medium text-white">Rate Limiting</h3>
|
||||
<Activity className={`w-4 h-4 ${status.rate_limit.enabled ? 'text-green-500' : 'text-gray-400'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold mb-1 text-white">
|
||||
{status.rate_limit.enabled ? 'Active' : 'Disabled'}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
DDoS Protection
|
||||
</p>
|
||||
{status.rate_limit.enabled && (
|
||||
<div className="mt-4">
|
||||
<Button variant="secondary" size="sm" className="w-full">
|
||||
Configure Limits
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user