From f426595013c49011237d0c4b3ef9c9e78785948e Mon Sep 17 00:00:00 2001
From: Wikid82
Date: Fri, 28 Nov 2025 15:18:44 -0500
Subject: [PATCH 01/11] fix: update README to reflect project name change and
improve clarity
---
README.md | 73 +++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 57 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 22de5ee7..35f1e938 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-
+
-Caddy Proxy Manager+
+Charon
The friendly way to manage your reverse proxy
@@ -11,8 +11,8 @@
-
-
+
+
---
@@ -23,7 +23,7 @@
|---------|-------------|
| π¨ **Beautiful Dark UI** | Modern interface that's easy on the eyes, works on any device |
| π **Automatic HTTPS** | Free SSL certificates from Let's Encrypt, auto-renewed |
-| π‘οΈ **Built-in Security** | CrowdSec integration, geo-blocking, IP access lists |
+| π‘οΈ **Built-in Security** | CrowdSec integration, geo-blocking, IP access lists (optional, powered by Cerberus) |
| π **Uptime Monitoring** | Know when your services go down with smart notifications |
| π³ **Docker Discovery** | Auto-detect containers on local and remote Docker hosts |
| π₯ **Easy Import** | Bring your existing Caddy or NPM configs with one click |
@@ -32,29 +32,69 @@
| π **WebSocket Support** | Perfect for real-time apps and chat services |
| β‘ **Zero Downtime** | Hot-reload configuration without restarts |
-**[See all features β](https://wikid82.github.io/cpmp/docs/features.html)**
+**[See all features β](https://wikid82.github.io/charon/features)**
---
## π Quick Start
```bash
-# Clone and start
-git clone https://github.com/Wikid82/cpmp.git
-cd cpmp
-docker compose up -d
+services:
+ charon:
+ image: ghcr.io/wikid82/charon:latest
+ container_name: charon
+ restart: unless-stopped
+ ports:
+ - "80:80" # HTTP (Caddy proxy)
+ - "443:443" # HTTPS (Caddy proxy)
+ - "443:443/udp" # HTTP/3 (Caddy proxy)
+ - "8080:8080" # Management UI (Charon)
+ environment:
+ - CHARON_ENV=production # New env var prefix (CHARON_). CPM_ values still supported.
+ - TZ=UTC # Set timezone (e.g., America/New_York)
+ - CHARON_HTTP_PORT=8080
+ - CHARON_DB_PATH=/app/data/charon.db
+ - CHARON_FRONTEND_DIR=/app/frontend/dist
+ - CHARON_CADDY_ADMIN_API=http://localhost:2019
+ - CHARON_CADDY_CONFIG_DIR=/app/data/caddy
+ - CHARON_CADDY_BINARY=caddy
+ - CHARON_IMPORT_CADDYFILE=/import/Caddyfile
+ - CHARON_IMPORT_DIR=/app/data/imports
+ # Security Services (Optional)
+ #- CERBERUS_SECURITY_CROWDSEC_MODE=disabled # disabled, local, external
+ #- CERBERUS_SECURITY_CROWDSEC_API_URL= # Required if mode is external
+ #- CERBERUS_SECURITY_CROWDSEC_API_KEY= # Required if mode is external
+ #- CERBERUS_SECURITY_WAF_MODE=disabled # disabled, enabled
+ #- CERBERUS_SECURITY_RATELIMIT_ENABLED=false
+ #- CERBERUS_SECURITY_ACL_ENABLED=false
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ volumes:
+ - :/app/data
+ - :/data
+ - :/config
+ - /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
+ # Mount your existing Caddyfile for automatic import (optional)
+ # - ./my-existing-Caddyfile:/import/Caddyfile:ro
+ # - ./sites:/import/sites:ro # If your Caddyfile imports other files
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
```
Open **http://localhost:8080** β that's it! π
-**[Full documentation β](https://wikid82.github.io/cpmp/docs/index.html)**
+**[Full documentation β](https://wikid82.github.io/charon/)**
---
## π¬ Community
-- π **Found a bug?** [Open an issue](https://github.com/Wikid82/cpmp/issues)
-- π‘ **Have an idea?** [Start a discussion](https://github.com/Wikid82/cpmp/discussions)
+- π **Found a bug?** [Open an issue](https://github.com/Wikid82/charon/issues)
+- π‘ **Have an idea?** [Start a discussion](https://github.com/Wikid82/charon/discussions)
- π **Roadmap** [View the project board](https://github.com/users/Wikid82/projects/7)
## π€ Contributing
@@ -65,11 +105,12 @@ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) to get s
MIT License Β·
- Documentation Β·
- Releases
+ Documentation Β·
+ Releases
Built with β€οΈ by @Wikid82
- Powered by Caddy Server Β· Inspired by Nginx Proxy Manager
+ Security Suite: Charon is powered by Cerberus β an optional security engine bundling WAF (Coraza), CrowdSec integrations, ACLs and rate-limiting.
+ Powered by Caddy Server Β· Inspired by Nginx Proxy Manager & Pangolin
From 19f884f88ca8dbd677dd84896c68af3712ba74e6 Mon Sep 17 00:00:00 2001
From: Wikid82
Date: Fri, 28 Nov 2025 15:37:49 -0500
Subject: [PATCH 02/11] fix: update README to enhance clarity and detail about
Charon and Cerberus
---
README.md | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 35f1e938..933de2bf 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,17 @@
Charon
-
- The friendly way to manage your reverse proxy
- Point, click, done. No config files needed.
-
+ The Gateway to Effortless Connectivity.
+
+
+Charon bridges the gap between the complex internet and your private services. Enjoy a simplified, visual management experience built specifically for the home server enthusiast. No code requiredβjust safe passage.
+
+Cerberus
+
+ The Guardian at the Gate.
+
+
+Ensure nothing passes without permission. Cerberus is a robust security suite featuring the Coraza WAF, deep CrowdSec integration, and granular rate-limiting. Always watching, always protecting.
@@ -111,6 +118,5 @@ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) to get s
Built with β€οΈ by @Wikid82
- Security Suite: Charon is powered by Cerberus β an optional security engine bundling WAF (Coraza), CrowdSec integrations, ACLs and rate-limiting.
Powered by Caddy Server Β· Inspired by Nginx Proxy Manager & Pangolin
From 1a2568ff4d9cac0b43dad94c659dd533bbd0deea Mon Sep 17 00:00:00 2001
From: Wikid82
Date: Fri, 28 Nov 2025 15:42:25 -0500
Subject: [PATCH 03/11] fix: correct header level for Cerberus section in
README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 933de2bf..b7b12cf0 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
Charon bridges the gap between the complex internet and your private services. Enjoy a simplified, visual management experience built specifically for the home server enthusiast. No code requiredβjust safe passage.
-Cerberus
+Cerberus
The Guardian at the Gate.
From fc1bf92bd675e009e457ce51e76916a4ceed8852 Mon Sep 17 00:00:00 2001
From: Wikid82
Date: Fri, 28 Nov 2025 15:43:46 -0500
Subject: [PATCH 04/11] fix: add spacing before license and release badges in
README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index b7b12cf0..4a92f278 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Charon bridges the gap between the complex internet and your private services. E
Ensure nothing passes without permission. Cerberus is a robust security suite featuring the Coraza WAF, deep CrowdSec integration, and granular rate-limiting. Always watching, always protecting.
-
+
From 62bbd6693ea9ce45a3a4b6a15e7f58c005ab2b10 Mon Sep 17 00:00:00 2001
From: Wikid82
Date: Fri, 28 Nov 2025 15:50:12 -0500
Subject: [PATCH 05/11] fix: reorder feature list in README for improved
clarity
---
README.md | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 4a92f278..bf746875 100644
--- a/README.md
+++ b/README.md
@@ -28,16 +28,16 @@ Ensure nothing passes without permission. Cerberus is a robust security suite fe
| Feature | Description |
|---------|-------------|
-| π¨ **Beautiful Dark UI** | Modern interface that's easy on the eyes, works on any device |
| π **Automatic HTTPS** | Free SSL certificates from Let's Encrypt, auto-renewed |
| π‘οΈ **Built-in Security** | CrowdSec integration, geo-blocking, IP access lists (optional, powered by Cerberus) |
-| π **Uptime Monitoring** | Know when your services go down with smart notifications |
-| π³ **Docker Discovery** | Auto-detect containers on local and remote Docker hosts |
-| π₯ **Easy Import** | Bring your existing Caddy or NPM configs with one click |
-| πΎ **Backup & Restore** | Never lose your settings, export anytime |
-| π **Health Checks** | Test connections before saving |
-| π **WebSocket Support** | Perfect for real-time apps and chat services |
| β‘ **Zero Downtime** | Hot-reload configuration without restarts |
+| π³ **Docker Discovery** | Auto-detect containers on local and remote Docker hosts |
+| π **Uptime Monitoring** | Know when your services go down with smart notifications |
+| π **Health Checks** | Test connections before saving |
+| π₯ **Easy Import** | Bring your existing Caddy configs with one click |
+| πΎ **Backup & Restore** | Never lose your settings, export anytime |
+| π **WebSocket Support** | Perfect for real-time apps and chat services |
+| π¨ **Beautiful Dark UI** | Modern interface that's easy on the eyes, works on any device |
**[See all features β](https://wikid82.github.io/charon/features)**
From 78f7f7ecadc11ab5eeaf2df030734cfe68a18489 Mon Sep 17 00:00:00 2001
From: CI
Date: Sat, 29 Nov 2025 17:11:05 +0000
Subject: [PATCH 06/11] chore(ci): CodeQL setup + seed + integration test
(cherry-pick)
(cherry picked from commit 35957bf196c6aee928d66e723f3074c2b2935d68)
---
.github/workflows/codeql.yml | 6 +--
backend/cmd/seed/main.go | 6 +--
scripts/integration-test.sh | 77 ++++++++++++++++++++++++++++++++++++
3 files changed, 83 insertions(+), 6 deletions(-)
create mode 100755 scripts/integration-test.sh
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 6c609b5e..d17a521a 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -34,14 +34,14 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Initialize CodeQL
- uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4
+ uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
- uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4
+ uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4
+ uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4
with:
category: "/language:${{ matrix.language }}"
diff --git a/backend/cmd/seed/main.go b/backend/cmd/seed/main.go
index 5b12b34c..68a16688 100644
--- a/backend/cmd/seed/main.go
+++ b/backend/cmd/seed/main.go
@@ -8,12 +8,12 @@ import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
- "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
+ "github.com/Wikid82/charon/backend/internal/models"
)
func main() {
// Connect to database
- db, err := gorm.Open(sqlite.Open("./data/cpm.db"), &gorm.Config{})
+ db, err := gorm.Open(sqlite.Open("./data/charon.db"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
@@ -152,7 +152,7 @@ func main() {
settings := []models.Setting{
{
Key: "app_name",
- Value: "Caddy Proxy Manager+",
+ Value: "Charon",
Type: "string",
Category: "general",
},
diff --git a/scripts/integration-test.sh b/scripts/integration-test.sh
new file mode 100755
index 00000000..2ff8c7da
--- /dev/null
+++ b/scripts/integration-test.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+set -e
+
+# Configuration
+API_URL="http://localhost:8080/api/v1"
+ADMIN_EMAIL="admin@example.com"
+ADMIN_PASSWORD="changeme"
+
+echo "Waiting for Charon to be ready..."
+for i in $(seq 1 30); do
+ code=$(curl -s -o /dev/null -w "%{http_code}" $API_URL/health || echo "000")
+ if [ "$code" = "200" ]; then
+ echo "β
Charon is ready!"
+ break
+ fi
+ echo "Attempt $i/30: health not ready (code=$code); waiting..."
+ sleep 2
+done
+
+if [ "$code" != "200" ]; then
+ echo "β Charon failed to start"
+ exit 1
+fi
+
+echo "Logging in..."
+TOKEN=$(curl -s -X POST $API_URL/auth/login \
+ -H "Content-Type: application/json" \
+ -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASSWORD\"}" | jq -r .token)
+
+if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
+ echo "β Login failed"
+ exit 1
+fi
+echo "β
Login successful"
+
+echo "Creating Proxy Host..."
+# We use 'whoami' as the forward host because they are on the same docker network
+RESPONSE=$(curl -s -X POST $API_URL/proxy-hosts \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "domain_names": ["test.localhost"],
+ "forward_scheme": "http",
+ "forward_host": "whoami",
+ "forward_port": 80,
+ "access_list_id": "",
+ "certificate_id": "",
+ "ssl_forced": false,
+ "caching_enabled": false,
+ "block_exploits": false,
+ "allow_websocket_upgrade": true,
+ "http2_support": true,
+ "hsts_enabled": false,
+ "hsts_subdomains": false,
+ "locations": []
+ }')
+
+ID=$(echo $RESPONSE | jq -r .uuid)
+if [ -z "$ID" ] || [ "$ID" = "null" ]; then
+ echo "β Failed to create proxy host: $RESPONSE"
+ exit 1
+fi
+echo "β
Proxy Host created (ID: $ID)"
+
+echo "Testing Proxy..."
+# We use Host header to route to the correct proxy host
+# We hit localhost:80 (Caddy) which should route to whoami
+HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: test.localhost" http://localhost:80)
+CONTENT=$(curl -s -H "Host: test.localhost" http://localhost:80)
+
+if [ "$HTTP_CODE" = "200" ] && echo "$CONTENT" | grep -q "Hostname:"; then
+ echo "β
Proxy test passed! Content received from whoami."
+else
+ echo "β Proxy test failed (Code: $HTTP_CODE)"
+ echo "Content: $CONTENT"
+ exit 1
+fi
From a60be34f6061abf3bfa43f166ec09edd25836ee0 Mon Sep 17 00:00:00 2001
From: CI
Date: Sat, 29 Nov 2025 21:23:54 +0000
Subject: [PATCH 07/11] chore(ci): add PR-only Trivy app-only scan and pin
Caddy v2.10.2
---
.github/workflows/docker-publish.yml | 117 ++++++++++++++++++---------
Dockerfile | 100 ++++++++++++++++-------
2 files changed, 154 insertions(+), 63 deletions(-)
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 7079c13d..55e0c7df 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -6,8 +6,7 @@ on:
- main
- development
- feature/beta-release
- tags:
- - 'v*.*.*'
+ # Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds
pull_request:
branches:
- main
@@ -18,7 +17,7 @@ on:
env:
REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository_owner }}/cpmp
+ IMAGE_NAME: ${{ github.repository_owner }}/charon
jobs:
build-and-push:
@@ -35,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Normalize image name
run: |
@@ -70,11 +69,11 @@ jobs:
- name: Set up QEMU
if: steps.skip.outputs.skip_build != 'true'
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
+ uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
if: steps.skip.outputs.skip_build != 'true'
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
+ uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Resolve Caddy base digest
if: steps.skip.outputs.skip_build != 'true'
@@ -84,34 +83,42 @@ jobs:
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
echo "image=$DIGEST" >> $GITHUB_OUTPUT
+ - name: Choose Registry Token
+ if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
+ run: |
+ if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then
+ echo "Using CHARON_TOKEN" >&2
+ echo "REGISTRY_PASSWORD=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV
+ else
+ echo "Using CPMP_TOKEN fallback" >&2
+ echo "REGISTRY_PASSWORD=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV
+ fi
+
- name: Log in to Container Registry
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
- password: ${{ secrets.CPMP_TOKEN }}
+ password: ${{ env.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels)
if: steps.skip.outputs.skip_build != 'true'
id: meta
- uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
+ uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }}
- type=semver,pattern={{version}}
- type=semver,pattern={{major}}.{{minor}}
- type=semver,pattern={{major}}
type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }}
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
- name: Build and push Docker image
if: steps.skip.outputs.skip_build != 'true'
id: build-and-push
- uses: docker/build-push-action@v6 # v6.9.0
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
@@ -128,7 +135,7 @@ jobs:
- name: Run Trivy scan (table output)
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
- uses: aquasecurity/trivy-action@0.28.0 # 0.28.0
+ uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
format: 'table'
@@ -139,7 +146,7 @@ jobs:
- name: Run Trivy vulnerability scanner (SARIF)
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
id: trivy
- uses: aquasecurity/trivy-action@0.28.0 # 0.28.0
+ uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
format: 'sarif'
@@ -159,7 +166,7 @@ jobs:
- name: Upload Trivy results
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
- uses: github/codeql-action/upload-sarif@f079b8493333aace61c81488f8bd40919487bd9f # v3.26.13
+ uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
sarif_file: 'trivy-results.sarif'
token: ${{ secrets.GITHUB_TOKEN }}
@@ -184,6 +191,9 @@ jobs:
if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request'
steps:
+ - name: Checkout repository
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
+
- name: Normalize image name
run: |
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
@@ -202,38 +212,47 @@ jobs:
echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
fi
+ - name: Choose Registry Token
+ run: |
+ if [ -n "${{ secrets.CHARON_TOKEN }}" ]; then
+ echo "Using CHARON_TOKEN" >&2
+ echo "REGISTRY_PASSWORD=${{ secrets.CHARON_TOKEN }}" >> $GITHUB_ENV
+ else
+ echo "Using CPMP_TOKEN fallback" >&2
+ echo "REGISTRY_PASSWORD=${{ secrets.CPMP_TOKEN }}" >> $GITHUB_ENV
+ fi
+
- name: Log in to GitHub Container Registry
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
- password: ${{ secrets.CPMP_TOKEN }}
+ password: ${{ env.REGISTRY_PASSWORD }}
- name: Pull Docker image
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
- - name: Run container
+ - name: Create Docker Network
+ run: docker network create charon-test-net
+
+ - name: Run Upstream Service (whoami)
+ run: |
+ docker run -d \
+ --name whoami \
+ --network charon-test-net \
+ traefik/whoami
+
+ - name: Run Charon Container
run: |
docker run -d \
--name test-container \
+ --network charon-test-net \
-p 8080:8080 \
+ -p 80:80 \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
- - name: Test health endpoint (retries)
- run: |
- set +e
- for i in $(seq 1 30); do
- code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000")
- if [ "$code" = "200" ]; then
- echo "β
Health check passed on attempt $i"
- exit 0
- fi
- echo "Attempt $i/30: health not ready (code=$code); waiting..."
- sleep 2
- done
- echo "β Health check failed after retries"
- docker logs test-container || true
- exit 1
+ - name: Run Integration Test
+ run: ./scripts/integration-test.sh
- name: Check container logs
if: always()
@@ -241,7 +260,10 @@ jobs:
- name: Stop container
if: always()
- run: docker stop test-container && docker rm test-container
+ run: |
+ docker stop test-container whoami || true
+ docker rm test-container whoami || true
+ docker network rm charon-test-net || true
- name: Create test summary
if: always()
@@ -249,4 +271,27 @@ jobs:
echo "## π§ͺ Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Health Check**: ${{ job.status == 'success' && 'β
Passed' || 'β Failed' }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Integration Test**: ${{ job.status == 'success' && 'β
Passed' || 'β Failed' }}" >> $GITHUB_STEP_SUMMARY
+
+ trivy-pr-app-only:
+ name: Trivy (PR) - App-only
+ runs-on: ubuntu-latest
+ if: github.event_name == 'pull_request'
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Build image locally for PR
+ run: |
+ docker build -t charon:pr-${{ github.sha }} .
+
+ - name: Extract `charon` binary from image
+ run: |
+ CONTAINER=$(docker create charon:pr-${{ github.sha }})
+ docker cp ${CONTAINER}:/app/charon ./charon_binary || true
+ docker rm ${CONTAINER} || true
+
+ - name: Run Trivy filesystem scan on `charon` (fail PR on HIGH/CRITICAL)
+ run: |
+ docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy -v $PWD:/workdir aquasec/trivy:latest fs --exit-code 1 --severity CRITICAL,HIGH /workdir/charon_binary
+ shell: bash
diff --git a/Dockerfile b/Dockerfile
index 675ddce6..03d04a4b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-# Multi-stage Dockerfile for CaddyProxyManager+ with integrated Caddy
+# Multi-stage Dockerfile for Charon with integrated Caddy
# Single container deployment for simplified home user setup
# Build arguments for versioning
@@ -6,9 +6,19 @@ ARG VERSION=dev
ARG BUILD_DATE
ARG VCS_REF
-# Allow pinning Caddy base image by digest via build-arg
-# Using caddy:2.9.1-alpine to fix CVE-2025-59530 and stdlib vulnerabilities
-ARG CADDY_IMAGE=caddy:2.9.1-alpine
+# Allow pinning Caddy version - Renovate will update this
+# Build the most recent Caddy 2.x release (keeps major pinned under v3).
+# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
+# avoid accidentally pulling a v3 major release. Renovate can still update
+# this ARG to a specific v2.x tag when desired.
+## 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.10.2 build.
+ARG CADDY_VERSION=2.10.2
+## 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
+## upstream caddy image tags while still shipping a pinned caddy binary.
+ARG CADDY_IMAGE=alpine:3.18
# ---- Cross-Compilation Helpers ----
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.8.0 AS xx
@@ -42,12 +52,15 @@ WORKDIR /app/backend
# Install build dependencies
# xx-apk installs packages for the TARGET architecture
ARG TARGETPLATFORM
+# hadolint ignore=DL3018
RUN apk add --no-cache clang lld
+# hadolint ignore=DL3018,DL3059
RUN xx-apk add --no-cache gcc musl-dev sqlite-dev
# Install Delve (cross-compile for target)
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
+# hadolint ignore=DL3059,DL4006
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@latest && \
DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
if [ -n "$DLV_PATH" ] && [ "$DLV_PATH" != "/go/bin/dlv" ]; then \
@@ -68,17 +81,14 @@ ARG VCS_REF=unknown
ARG BUILD_DATE=unknown
# Build the Go binary with version information injected via ldflags
-# -gcflags "all=-N -l" disables optimizations and inlining for better debugging
# xx-go handles CGO and cross-compilation flags automatically
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=1 xx-go build \
- -gcflags "all=-N -l" \
- -a -installsuffix cgo \
- -ldflags "-X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${VERSION} \
- -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.GitCommit=${VCS_REF} \
- -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.BuildTime=${BUILD_DATE}" \
- -o cpmp ./cmd/api
+ -ldflags "-s -w -X github.com/Wikid82/charon/backend/internal/version.Version=${VERSION} \
+ -X github.com/Wikid82/charon/backend/internal/version.GitCommit=${VCS_REF} \
+ -X github.com/Wikid82/charon/backend/internal/version.BuildTime=${BUILD_DATE}" \
+ -o charon ./cmd/api
# ---- Caddy Builder ----
# Build Caddy from source to ensure we use the latest Go version and dependencies
@@ -86,32 +96,60 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
FROM --platform=$BUILDPLATFORM golang:alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
+ARG CADDY_VERSION
+# hadolint ignore=DL3018
RUN apk add --no-cache git
+# hadolint ignore=DL3062
RUN --mount=type=cache,target=/go/pkg/mod \
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
-# Build Caddy for the target architecture
+# Pre-fetch/override vulnerable module versions in the module cache so xcaddy
+# will pick them up during the build. These `go get` calls attempt to pin
+# fixed versions of dependencies known to cause Trivy findings (expr, quic-go).
+RUN --mount=type=cache,target=/go/pkg/mod \
+ go get github.com/expr-lang/expr@v1.17.0 github.com/quic-go/quic-go@v0.54.1 || true
+
+# Build Caddy for the target architecture with security plugins.
+# Try the requested v${CADDY_VERSION} tag first; if it fails (unknown tag),
+# fall back to a known-good v2.10.2 build to keep the build resilient.
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 \
- --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
+ --mount=type=cache,target=/go/pkg/mod \
+ sh -c "GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_VERSION} \
+ --with github.com/greenpau/caddy-security \
+ --with github.com/corazawaf/coraza-caddy/v2 \
+ --with github.com/hslatman/caddy-crowdsec-bouncer \
+ --with github.com/zhangjiayin/caddy-geoip2 \
+ --output /usr/bin/caddy || \
+ (echo 'Requested Caddy tag v${CADDY_VERSION} failed; falling back to v2.10.2' && \
+ GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v2.10.2 \
+ --with github.com/greenpau/caddy-security \
+ --with github.com/corazawaf/coraza-caddy/v2 \
+ --with github.com/hslatman/caddy-crowdsec-bouncer \
+ --with github.com/zhangjiayin/caddy-geoip2 --output /usr/bin/caddy)"
# ---- Final Runtime with Caddy ----
FROM ${CADDY_IMAGE}
WORKDIR /app
-# Install runtime dependencies for CPM+ (no bash needed)
-RUN apk --no-cache add ca-certificates sqlite-libs \
+# Install runtime dependencies for Charon (no bash needed)
+# hadolint ignore=DL3018
+RUN apk --no-cache add ca-certificates sqlite-libs tzdata curl \
&& apk --no-cache upgrade
+# Download MaxMind GeoLite2 Country database
+# Note: In production, users should provide their own MaxMind license key
+# This uses the publicly available GeoLite2 database
+RUN mkdir -p /app/data/geoip && \
+ curl -L "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
+ -o /app/data/geoip/GeoLite2-Country.mmdb
+
# Copy Caddy binary from caddy-builder (overwriting the one from base image)
COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
# Copy Go binary from backend builder
-COPY --from=backend-builder /app/backend/cpmp /app/cpmp
+COPY --from=backend-builder /app/backend/charon /app/charon
+RUN ln -s /app/charon /app/cpmp || true
# Copy Delve debugger (xx-go install places it in /go/bin)
COPY --from=backend-builder /go/bin/dlv /usr/local/bin/dlv
@@ -123,12 +161,20 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Set default environment variables
-ENV CPM_ENV=production \
+ENV CHARON_ENV=production \
+ CHARON_HTTP_PORT=8080 \
+ CHARON_DB_PATH=/app/data/charon.db \
+ CHARON_FRONTEND_DIR=/app/frontend/dist \
+ CHARON_CADDY_ADMIN_API=http://localhost:2019 \
+ CHARON_CADDY_CONFIG_DIR=/app/data/caddy \
+ CHARON_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb \
+ CPM_ENV=production \
CPM_HTTP_PORT=8080 \
CPM_DB_PATH=/app/data/cpm.db \
CPM_FRONTEND_DIR=/app/frontend/dist \
CPM_CADDY_ADMIN_API=http://localhost:2019 \
- CPM_CADDY_CONFIG_DIR=/app/data/caddy
+ CPM_CADDY_CONFIG_DIR=/app/data/caddy \
+ CPM_GEOIP_DB_PATH=/app/data/geoip/GeoLite2-Country.mmdb
# Create necessary directories
RUN mkdir -p /app/data /app/data/caddy /config
@@ -139,18 +185,18 @@ ARG BUILD_DATE
ARG VCS_REF
# OCI image labels for version metadata
-LABEL org.opencontainers.image.title="CaddyProxyManager+ (CPMP)" \
+LABEL org.opencontainers.image.title="Charon (CPMP legacy)" \
org.opencontainers.image.description="Web UI for managing Caddy reverse proxy configurations" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
- org.opencontainers.image.source="https://github.com/Wikid82/CaddyProxyManagerPlus" \
- org.opencontainers.image.url="https://github.com/Wikid82/CaddyProxyManagerPlus" \
- org.opencontainers.image.vendor="CaddyProxyManagerPlus" \
+ org.opencontainers.image.source="https://github.com/Wikid82/charon" \
+ org.opencontainers.image.url="https://github.com/Wikid82/charon" \
+ org.opencontainers.image.vendor="charon" \
org.opencontainers.image.licenses="MIT"
# Expose ports
EXPOSE 80 443 443/udp 8080 2019
-# Use custom entrypoint to start both Caddy and CPM+
+# Use custom entrypoint to start both Caddy and Charon
ENTRYPOINT ["/docker-entrypoint.sh"]
From 4595fd4dd0bcfcecde2856d81777ec09e4b88087 Mon Sep 17 00:00:00 2001
From: Jeremy
Date: Sat, 29 Nov 2025 17:53:54 -0500
Subject: [PATCH 08/11] Update .goreleaser.yaml
---
.goreleaser.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index b16bbf30..85171bf1 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -1,4 +1,4 @@
-version: 2
+version: 1
project_name: charon
From 7b4145606fc194addb167950ae11030545dd1af1 Mon Sep 17 00:00:00 2001
From: CI
Date: Sat, 29 Nov 2025 23:48:33 +0000
Subject: [PATCH 09/11] ci: set git identity for auto-versioning and normalize
tags; update .gitignore
---
.github/workflows/auto-versioning.yml | 12 +++++++++++-
.gitignore | 1 -
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/auto-versioning.yml b/.github/workflows/auto-versioning.yml
index 781e4640..28a05a23 100644
--- a/.github/workflows/auto-versioning.yml
+++ b/.github/workflows/auto-versioning.yml
@@ -37,7 +37,17 @@ jobs:
- name: Create annotated tag and push
if: ${{ steps.semver.outputs.changed }}
run: |
- git tag -a v${{ steps.semver.outputs.version }} -m "Release v${{ steps.semver.outputs.version }}"
+ # Ensure a committer identity is configured in the runner so git tag works
+ git config --global user.email "actions@github.com"
+ git config --global user.name "GitHub Actions"
+
+ # Normalize the version: remove any leading 'v' so we don't end up with 'vvX.Y.Z'
+ RAW="${{ steps.semver.outputs.version }}"
+ VERSION_NO_V="${RAW#v}"
+
+ TAG="v${VERSION_NO_V}"
+ echo "Creating tag: ${TAG}"
+ git tag -a "${TAG}" -m "Release ${TAG}"
git push origin --tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index acdcb2a4..2ea3fb99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,4 +118,3 @@ PROJECT_PLANNING.md
SECURITY_IMPLEMENTATION_PLAN.md
VERSIONING_IMPLEMENTATION.md
backend/internal/api/handlers/import_handler.go.bak
-docker-compose.local.yml
From eacf80ea2a8b6ee9f835ca04f189a74fc16560a1 Mon Sep 17 00:00:00 2001
From: CI
Date: Sat, 29 Nov 2025 23:48:57 +0000
Subject: [PATCH 10/11] feat: add docker-compose configuration for local
development environment
---
docker-compose.local.yml | 58 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 docker-compose.local.yml
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
new file mode 100644
index 00000000..e9f6ecf3
--- /dev/null
+++ b/docker-compose.local.yml
@@ -0,0 +1,58 @@
+services:
+ app:
+ image: cpmp:local
+ container_name: cpmp-debug
+ restart: unless-stopped
+ ports:
+ - "80:80" # HTTP (Caddy proxy)
+ - "443:443" # HTTPS (Caddy proxy)
+ - "443:443/udp" # HTTP/3 (Caddy proxy)
+ - "8080:8080" # Management UI (CPM+)
+ - "2345:2345" # Delve Debugger
+ environment:
+ - CPM_ENV=development
+ - CPMP_DEBUG=1
+ - TZ=America/New_York
+ - CPM_HTTP_PORT=8080
+ - CPM_DB_PATH=/app/data/cpm.db
+ - CPM_FRONTEND_DIR=/app/frontend/dist
+ - CPM_CADDY_ADMIN_API=http://localhost:2019
+ - CPM_CADDY_CONFIG_DIR=/app/data/caddy
+ - CPM_CADDY_BINARY=caddy
+ - CPM_IMPORT_CADDYFILE=/import/Caddyfile
+ - CPM_IMPORT_DIR=/app/data/imports
+ - CPM_ACME_STAGING=false
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ cap_add:
+ - SYS_PTRACE
+ security_opt:
+ - seccomp:unconfined
+ volumes:
+ - cpm_data_local:/app/data
+ - caddy_data_local:/data
+ - caddy_config_local:/config
+ - /var/run/docker.sock:/var/run/docker.sock:ro # For local container discovery
+ - ./backend:/app/backend:ro # Mount source for debugging
+ # Mount your existing Caddyfile for automatic import (optional)
+# - :/import/Caddyfile:ro
+# - :/import/sites:ro # If your Caddyfile imports other files
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/v1/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+
+volumes:
+ cpm_data_local:
+ driver: local
+ caddy_data_local:
+ driver: local
+ caddy_config_local:
+ driver: local
+
+networks:
+ default:
+ name: containers_default
+ external: true
From 7abdfe29d666377dffeedc9966fa246408439577 Mon Sep 17 00:00:00 2001
From: CI
Date: Sat, 29 Nov 2025 23:50:38 +0000
Subject: [PATCH 11/11] ci: skip existing tag creation and use normalized TAG
for release
---
.github/workflows/auto-versioning.yml | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/auto-versioning.yml b/.github/workflows/auto-versioning.yml
index 28a05a23..030b62c1 100644
--- a/.github/workflows/auto-versioning.yml
+++ b/.github/workflows/auto-versioning.yml
@@ -34,7 +34,8 @@ jobs:
run: |
echo "Next version: ${{ steps.semver.outputs.version }}"
- - name: Create annotated tag and push
+ - id: create_tag
+ name: Create annotated tag and push
if: ${{ steps.semver.outputs.changed }}
run: |
# Ensure a committer identity is configured in the runner so git tag works
@@ -46,9 +47,18 @@ jobs:
VERSION_NO_V="${RAW#v}"
TAG="v${VERSION_NO_V}"
- echo "Creating tag: ${TAG}"
- git tag -a "${TAG}" -m "Release ${TAG}"
- git push origin --tags
+ echo "TAG=${TAG}"
+
+ # If tag already exists, skip creation to avoid failure
+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
+ echo "Tag ${TAG} already exists; skipping tag creation"
+ else
+ git tag -a "${TAG}" -m "Release ${TAG}"
+ git push origin "${TAG}"
+ fi
+
+ # Export the tag for downstream steps
+ echo "tag=${TAG}" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -56,8 +66,8 @@ jobs:
if: ${{ steps.semver.outputs.changed }}
uses: softprops/action-gh-release@v1
with:
- tag_name: ${{ steps.semver.outputs.version }}
- name: Release ${{ steps.semver.outputs.version }}
+ tag_name: ${{ steps.create_tag.outputs.tag }}
+ name: Release ${{ steps.create_tag.outputs.tag }}
body: ${{ steps.semver.outputs.release_notes }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}