Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
51d997c
chore: Update current spec to outline Caddy 2.11.1 compatibility, sec…
actions-user Feb 23, 2026
3fa1074
Merge branch 'development' into feature/beta-release
Wikid82 Feb 23, 2026
427babd
Merge pull request #742 from Wikid82/development
Wikid82 Feb 23, 2026
45458df
chore: Add Caddy compatibility gate workflow and related scripts; enh…
actions-user Feb 23, 2026
735b9fd
chore(deps): update non-major-updates
renovate[bot] Feb 23, 2026
441c3dc
Merge pull request #747 from Wikid82/renovate/feature/beta-release-no…
Wikid82 Feb 23, 2026
1f2b4c7
chore: Add Caddy compatibility gate workflow and related scripts; upd…
actions-user Feb 23, 2026
7b640cc
chore: Add Prettier and Tailwind CSS plugin to devDependencies
actions-user Feb 23, 2026
79c8e66
chore: Update minimum coverage requirements to 87% for backend and fr…
actions-user Feb 23, 2026
63d7c5c
chore: Update Caddy patch scenario and enhance CaddyAdminAPI validati…
actions-user Feb 23, 2026
1315d7a
chore: Add cache dependency path for Go setup in workflows
actions-user Feb 23, 2026
8fa0950
chore(deps): update github/codeql-action digest to a754a57
renovate[bot] Feb 23, 2026
9424aca
Merge pull request #748 from Wikid82/renovate/feature/beta-release-no…
Wikid82 Feb 23, 2026
ee5350d
feat: add keepalive controls to System Settings
actions-user Feb 23, 2026
9e71dd2
chore: update katex to version 0.16.33 in package-lock.json
actions-user Feb 23, 2026
09f9f7e
chore: remove Caddy Compatibility Gate workflow
actions-user Feb 23, 2026
72bfca2
fix(deps): update non-major-updates
renovate[bot] Feb 23, 2026
dc1426a
Merge pull request #749 from Wikid82/renovate/feature/beta-release-no…
Wikid82 Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Configure the application via `docker-compose.yml`:
| `CHARON_ENV` | `production` | Set to `development` for verbose logging (`CPM_ENV` supported for backward compatibility). |
| `CHARON_HTTP_PORT` | `8080` | Port for the Web UI (`CPM_HTTP_PORT` supported for backward compatibility). |
| `CHARON_DB_PATH` | `/app/data/charon.db` | Path to the SQLite database (`CPM_DB_PATH` supported for backward compatibility). |
| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). |
| `CHARON_CADDY_ADMIN_API` | `http://localhost:2019` | Internal URL for Caddy API (`CPM_CADDY_ADMIN_API` supported for backward compatibility). Must resolve to an internal allowlisted host on port `2019`. |
| `CHARON_CADDY_CONFIG_ROOT` | `/config` | Path to Caddy autosave configuration directory. |
| `CHARON_CADDY_LOG_DIR` | `/var/log/caddy` | Directory for Caddy access logs. |
| `CHARON_CROWDSEC_LOG_DIR` | `/var/log/crowdsec` | Directory for CrowdSec logs. |
Expand Down Expand Up @@ -218,6 +218,8 @@ environment:
- CPM_CADDY_ADMIN_API=http://your-caddy-host:2019
```

If using a non-localhost internal hostname, add it to `CHARON_SSRF_INTERNAL_HOST_ALLOWLIST`.

**Warning**: Charon will replace Caddy's entire configuration. Backup first!

## Performance Tuning
Expand Down
2 changes: 1 addition & 1 deletion .github/skills/test-backend-coverage-scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ cd "${PROJECT_ROOT}"
validate_project_structure "backend" "scripts/go-test-coverage.sh" || error_exit "Invalid project structure"

# Set default environment variables
set_default_env "CHARON_MIN_COVERAGE" "85"
set_default_env "CHARON_MIN_COVERAGE" "87"
set_default_env "PERF_MAX_MS_GETSTATUS_P95" "25ms"
set_default_env "PERF_MAX_MS_GETSTATUS_P95_PARALLEL" "50ms"
set_default_env "PERF_MAX_MS_LISTDECISIONS_P95" "75ms"
Expand Down
2 changes: 1 addition & 1 deletion .github/skills/test-frontend-coverage-scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ cd "${PROJECT_ROOT}"
validate_project_structure "frontend" "scripts/frontend-test-coverage.sh" || error_exit "Invalid project structure"

# Set default environment variables
set_default_env "CHARON_MIN_COVERAGE" "85"
set_default_env "CHARON_MIN_COVERAGE" "87"

# Execute the legacy script
log_step "EXECUTION" "Running frontend tests with coverage"
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release-goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ permissions:

jobs:
goreleaser:
if: ${{ !contains(github.ref_name, '-candidate') && !contains(github.ref_name, '-rc') }}
runs-on: ubuntu-latest
env:
# Use the built-in GITHUB_TOKEN by default for GitHub API operations.
Expand All @@ -32,10 +33,22 @@ jobs:
with:
fetch-depth: 0

- name: Enforce PR-2 release promotion guard
env:
REPO_VARS_JSON: ${{ toJSON(vars) }}
run: |
PR2_GATE_STATUS="$(printf '%s' "$REPO_VARS_JSON" | jq -r '.CHARON_PR2_GATES_PASSED // "false"')"
if [[ "$PR2_GATE_STATUS" != "true" ]]; then
echo "::error::Releasable tag promotion is blocked until PR-2 security/retirement gates pass."
echo "::error::Set repository variable CHARON_PR2_GATES_PASSED=true only after PR-2 approval."
exit 1
fi

- name: Set up Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum

- name: Set up Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/renovate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
fetch-depth: 1

- name: Run Renovate
uses: renovatebot/github-action@d65ef9e20512193cc070238b49c3873a361cd50c # v46.1.1
uses: renovatebot/github-action@8d75b92f43899d483728e9a8a7fd44238020f6e6 # v46.1.2
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ jobs:
- name: Download PR image artifact
if: steps.check-artifact.outputs.artifact_exists == 'true'
# actions/download-artifact v4.1.8
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
uses: actions/download-artifact@ac21fcf45e0aaee541c0f7030558bdad38d77d6c
with:
name: ${{ steps.pr-info.outputs.is_push == 'true' && 'push-image' || format('pr-image-{0}', steps.pr-info.outputs.pr_number) }}
run-id: ${{ steps.check-artifact.outputs.run_id }}
Expand Down Expand Up @@ -280,7 +280,7 @@ jobs:
- name: Upload Trivy SARIF to GitHub Security
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
# github/codeql-action v4
uses: github/codeql-action/upload-sarif@710e2945787622b429f8982cacb154faa182de18
uses: github/codeql-action/upload-sarif@cb4e075f119f8bccbc942d49655b2cd4dc6e615a
with:
sarif_file: 'trivy-binary-results.sarif'
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event.workflow_run.head_branch) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ repos:
stages: [manual] # Only runs when explicitly called
- id: frontend-type-check
name: Frontend TypeScript Check
entry: bash -c 'cd frontend && npm run type-check'
entry: bash -c 'cd frontend && npx tsc --noEmit'
language: system
files: '^frontend/.*\.(ts|tsx)$'
pass_filenames: false
Expand Down
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.19.0
v0.19.1
7 changes: 7 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,13 @@
"group": "test",
"problemMatcher": []
},
{
"label": "Security: Caddy PR-1 Compatibility Matrix",
"type": "shell",
"command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.1 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-compatibility-matrix.md",
"group": "test",
"problemMatcher": []
},
{
"label": "Test: E2E Playwright (Skill)",
"type": "shell",
Expand Down
35 changes: 28 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ ARG BUILD_DEBUG=0
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
## If the requested tag isn't available, fall back to a known-good v2.11.0-beta.2 build.
ARG CADDY_VERSION=2.11.0-beta.2
ARG CADDY_CANDIDATE_VERSION=2.11.1
ARG CADDY_USE_CANDIDATE=0
ARG CADDY_PATCH_SCENARIO=B
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
Expand Down Expand Up @@ -196,6 +199,9 @@ FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION
ARG CADDY_CANDIDATE_VERSION
ARG CADDY_USE_CANDIDATE
ARG CADDY_PATCH_SCENARIO
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
ARG XCADDY_VERSION=0.4.5

Expand All @@ -213,10 +219,16 @@ RUN --mount=type=cache,target=/go/pkg/mod \
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
sh -c 'set -e; \
CADDY_TARGET_VERSION="${CADDY_VERSION}"; \
if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \
CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \
fi; \
echo "Using Caddy target version: v${CADDY_TARGET_VERSION}"; \
echo "Using Caddy patch scenario: ${CADDY_PATCH_SCENARIO}"; \
export XCADDY_SKIP_CLEANUP=1; \
echo "Stage 1: Generate go.mod with xcaddy..."; \
# Run xcaddy to generate the build directory and go.mod
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
--with github.com/greenpau/caddy-security \
--with github.com/corazawaf/coraza-caddy/v2 \
--with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \
Expand All @@ -239,12 +251,21 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
go get github.com/expr-lang/expr@v1.17.7; \
# renovate: datasource=go depName=github.com/hslatman/ipstore
go get github.com/hslatman/ipstore@v0.4.0; \
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
# uses legacy nebula APIs removed in nebula v1.10+, which causes compile
# failures in authority/provisioner. Keep this pinned to a known-compatible
# v1.9.x release until upstream stack supports nebula v1.10+.
# renovate: datasource=go depName=github.com/slackhq/nebula
go get github.com/slackhq/nebula@v1.9.7; \
if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
# Rollback scenario: keep explicit nebula pin if upstream compatibility regresses.
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
# uses legacy nebula APIs removed in nebula v1.10+, which causes compile
# failures in authority/provisioner. Keep this pinned to a known-compatible
# v1.9.x release until upstream stack supports nebula v1.10+.
# renovate: datasource=go depName=github.com/slackhq/nebula
go get github.com/slackhq/nebula@v1.9.7; \
elif [ "${CADDY_PATCH_SCENARIO}" = "B" ] || [ "${CADDY_PATCH_SCENARIO}" = "C" ]; then \
# Default PR-2 posture: retire explicit nebula pin and use upstream resolution.
echo "Skipping nebula pin for scenario ${CADDY_PATCH_SCENARIO}"; \
else \
echo "Unsupported CADDY_PATCH_SCENARIO=${CADDY_PATCH_SCENARIO}"; \
exit 1; \
fi; \
# Clean up go.mod and ensure all dependencies are resolved
go mod tidy; \
echo "Dependencies patched successfully"; \
Expand Down
70 changes: 70 additions & 0 deletions backend/internal/api/handlers/settings_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -37,6 +38,15 @@ type SettingsHandler struct {
DataRoot string
}

const (
settingCaddyKeepaliveIdle = "caddy.keepalive_idle"
settingCaddyKeepaliveCount = "caddy.keepalive_count"
minCaddyKeepaliveIdleDuration = time.Second
maxCaddyKeepaliveIdleDuration = 24 * time.Hour
minCaddyKeepaliveCount = 1
maxCaddyKeepaliveCount = 100
)

func NewSettingsHandler(db *gorm.DB) *SettingsHandler {
return &SettingsHandler{
DB: db,
Expand Down Expand Up @@ -109,6 +119,11 @@ func (h *SettingsHandler) UpdateSetting(c *gin.Context) {
}
}

if err := validateOptionalKeepaliveSetting(req.Key, req.Value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

setting := models.Setting{
Key: req.Key,
Value: req.Value,
Expand Down Expand Up @@ -247,6 +262,10 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
}
}

if err := validateOptionalKeepaliveSetting(key, value); err != nil {
return err
}

setting := models.Setting{
Key: key,
Value: value,
Expand Down Expand Up @@ -284,6 +303,10 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin_whitelist"})
return
}
if strings.Contains(err.Error(), "invalid caddy.keepalive_idle") || strings.Contains(err.Error(), "invalid caddy.keepalive_count") {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if respondPermissionError(c, h.SecuritySvc, "settings_save_failed", err, h.DataRoot) {
return
}
Expand Down Expand Up @@ -401,6 +424,53 @@ func validateAdminWhitelist(whitelist string) error {
return nil
}

func validateOptionalKeepaliveSetting(key, value string) error {
switch key {
case settingCaddyKeepaliveIdle:
return validateKeepaliveIdleValue(value)
case settingCaddyKeepaliveCount:
return validateKeepaliveCountValue(value)
default:
return nil
}
}

func validateKeepaliveIdleValue(value string) error {
idle := strings.TrimSpace(value)
if idle == "" {
return nil
}

d, err := time.ParseDuration(idle)
if err != nil {
return fmt.Errorf("invalid caddy.keepalive_idle")
}

if d < minCaddyKeepaliveIdleDuration || d > maxCaddyKeepaliveIdleDuration {
return fmt.Errorf("invalid caddy.keepalive_idle")
}

return nil
}

func validateKeepaliveCountValue(value string) error {
raw := strings.TrimSpace(value)
if raw == "" {
return nil
}

count, err := strconv.Atoi(raw)
if err != nil {
return fmt.Errorf("invalid caddy.keepalive_count")
}

if count < minCaddyKeepaliveCount || count > maxCaddyKeepaliveCount {
return fmt.Errorf("invalid caddy.keepalive_count")
}

return nil
}

func (h *SettingsHandler) syncAdminWhitelist(whitelist string) error {
return h.syncAdminWhitelistWithDB(h.DB, whitelist)
}
Expand Down
Loading
Loading