Skip to content

Commit 8261406

Browse files
authored
Merge pull request #1030 from Wikid82/development
feat(orthrus): server-side Docker proxy listener for agent sessions
2 parents 6d74500 + b15f676 commit 8261406

56 files changed

Lines changed: 4905 additions & 970 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/nightly-build.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,88 @@ jobs:
361361
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
362362
echo "✅ SBOM attached to Docker Hub nightly image"
363363
364+
build-and-push-nightly-orthrus:
365+
needs: sync-development-to-nightly
366+
if: needs.sync-development-to-nightly.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch'
367+
runs-on: ubuntu-latest
368+
env:
369+
ORTHRUS_IMAGE_NAME: wikid82/orthrus
370+
HAS_DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN != '' }}
371+
permissions:
372+
contents: read
373+
packages: write
374+
375+
steps:
376+
- name: Checkout nightly branch
377+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
378+
with:
379+
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
380+
fetch-depth: 0
381+
382+
- name: Set lowercase image name
383+
run: echo "ORTHRUS_IMAGE_NAME_LC=${ORTHRUS_IMAGE_NAME,,}" >> "$GITHUB_ENV"
384+
385+
- name: Set up QEMU
386+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
387+
388+
- name: Set up Docker Buildx
389+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
390+
391+
- name: Log in to GitHub Container Registry
392+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
393+
with:
394+
registry: ${{ env.GHCR_REGISTRY }}
395+
username: ${{ github.actor }}
396+
password: ${{ secrets.GITHUB_TOKEN }}
397+
398+
- name: Log in to Docker Hub
399+
if: env.HAS_DOCKERHUB_TOKEN == 'true'
400+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
401+
with:
402+
registry: docker.io
403+
username: ${{ secrets.DOCKERHUB_USERNAME }}
404+
password: ${{ secrets.DOCKERHUB_TOKEN }}
405+
406+
- name: Extract metadata
407+
id: meta
408+
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
409+
with:
410+
images: |
411+
${{ env.GHCR_REGISTRY }}/${{ env.ORTHRUS_IMAGE_NAME_LC }}
412+
${{ env.DOCKERHUB_REGISTRY }}/${{ env.ORTHRUS_IMAGE_NAME_LC }}
413+
tags: |
414+
type=raw,value=nightly
415+
type=raw,value=nightly-{{date 'YYYY-MM-DD'}}
416+
type=sha,prefix=nightly-,format=short
417+
labels: |
418+
org.opencontainers.image.title=Orthrus Agent Nightly
419+
org.opencontainers.image.description=Nightly build of the Orthrus reverse-proxy agent
420+
421+
- name: Build and push Orthrus nightly image
422+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
423+
with:
424+
context: ./agent
425+
file: ./agent/Dockerfile
426+
platforms: linux/amd64,linux/arm64
427+
push: true
428+
tags: ${{ steps.meta.outputs.tags }}
429+
labels: ${{ steps.meta.outputs.labels }}
430+
build-args: |
431+
VERSION=nightly-${{ github.sha }}
432+
GIT_COMMIT=${{ github.sha }}
433+
BUILD_DATE=${{ github.event.repository.pushed_at }}
434+
cache-from: type=gha
435+
cache-to: type=gha,mode=max
436+
437+
- name: Record Orthrus nightly image tags
438+
run: |
439+
{
440+
echo "## 🐕 Orthrus Nightly Image Tags"
441+
echo '```'
442+
echo "${{ steps.meta.outputs.tags }}"
443+
echo '```'
444+
} >> "$GITHUB_STEP_SUMMARY"
445+
364446
test-nightly-image:
365447
needs: build-and-push-nightly
366448
runs-on: ubuntu-latest

.github/workflows/orthrus-build.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name: Orthrus Agent — Build & Publish
33
# Mirrors the trigger and tagging strategy from docker-build.yml so that the
44
# Orthrus agent image always receives the same identifiable tags as the main
55
# Charon image (e.g. feature-hecate-abc1234), enabling coordinated manual
6-
# testing on remote servers.
6+
# testing on remote servers. The push trigger has no paths filter (matching
7+
# docker-build.yml); paths filtering is only applied to pull_request events.
78

89
"on":
910
pull_request:
@@ -13,9 +14,6 @@ name: Orthrus Agent — Build & Publish
1314
push:
1415
branches: [main, development]
1516
tags: ['v*']
16-
paths:
17-
- 'agent/**'
18-
- '.github/workflows/orthrus-build.yml'
1917
workflow_dispatch:
2018
inputs:
2119
reason:

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"docker.host": "unix:///run/user/1001/docker.sock",
3+
"go.goroot": "/home/jeremy/sdk/go1.26.3",
34
"gopls": {
45
"buildFlags": ["-tags=integration"]
56
},

Dockerfile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ ARG NPM_VERSION=11.11.1
3737
# avoid accidentally pulling a v3 major release. Renovate can still update
3838
# this ARG to a specific v2.x tag when desired.
3939
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
40-
## If the requested tag isn't available, fall back to a known-good v2.11.2 build.
41-
ARG CADDY_VERSION=2.11.2
42-
ARG CADDY_CANDIDATE_VERSION=2.11.2
40+
## If the requested tag isn't available, fall back to a known-good v2.11.3 build.
41+
# renovate: datasource=go depName=https://github.com/caddyserver/caddy
42+
ARG CADDY_VERSION=2.11.3
43+
# renovate: datasource=go depName=https://github.com/caddyserver/caddy
44+
ARG CADDY_CANDIDATE_VERSION=2.11.3
4345
ARG CADDY_USE_CANDIDATE=0
4446
ARG CADDY_PATCH_SCENARIO=B
4547
# renovate: datasource=go depName=github.com/greenpau/caddy-security
@@ -404,7 +406,7 @@ RUN go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION} && \
404406
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream
405407
go get github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.10 && \
406408
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs
407-
go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.73.0 && \
409+
go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.74.0 && \
408410
go get github.com/aws/aws-sdk-go-v2/service/kinesis@v1.43.7 && \
409411
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/s3
410412
go get github.com/aws/aws-sdk-go-v2/service/s3@v1.101.0 && \

SECURITY.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public disclosure.
2727

2828
## Known Vulnerabilities
2929

30-
Last reviewed: 2026-04-21
30+
Last reviewed: 2026-05-20
3131

3232
### [HIGH] CVE-2026-31790 · OpenSSL Vulnerability in Alpine Base Image
3333

@@ -195,6 +195,91 @@ attacker-controlled URLs. Charon's application logic does not use busybox wget.
195195
Monitor Alpine 3.23 for a patched busybox APK. No immediate action required. Practical risk to
196196
Charon users is negligible since the vulnerable code path is not exercised.
197197

198+
---
199+
200+
### [HIGH] CVE-2026-45135 · Caddy FastCGI Unsafe Unicode Handling in splitPos
201+
202+
| Field | Value |
203+
|--------------|-------|
204+
| **ID** | CVE-2026-45135 |
205+
| **Severity** | High · 8.1 |
206+
| **Status** | Fix in Dockerfile (v2.11.3) — Pending Image Rebuild |
207+
208+
**What**
209+
Caddy v2.11.2 contains unsafe Unicode handling in the FastCGI `splitPos` function. A
210+
network-reachable attacker (no privileges required, high attack complexity) can trigger
211+
potential confidentiality and integrity impact through malformed Unicode in FastCGI requests.
212+
213+
**Who**
214+
215+
- Discovered by: Automated scan (Trivy image scan)
216+
- Reported: 2026-05-20
217+
- Affects: Charon deployments using the FastCGI reverse proxy capability
218+
219+
**Where**
220+
221+
- Component: `github.com/caddyserver/caddy/v2` v2.11.2 (embedded in Charon binary via xcaddy)
222+
- Versions affected: Caddy < v2.11.3
223+
224+
**When**
225+
226+
- Discovered: 2026-05-20
227+
- Disclosed (if public): Public
228+
- Target fix: v2.11.3 (available)
229+
230+
**How**
231+
Caddy is not a Go module dependency — it is built from source via xcaddy in the Dockerfile
232+
multi-stage build. The `CADDY_VERSION` ARG in the Dockerfile **is already pinned to 2.11.3**,
233+
which contains the fix. The vulnerability only affects the stale `charon:local` image built
234+
before this Dockerfile change. A container image rebuild applies the fix.
235+
236+
**Planned Remediation**
237+
Rebuild the container image. No code changes required. The fix is already present in the
238+
Dockerfile (`ARG CADDY_VERSION=2.11.3`).
239+
240+
---
241+
242+
### [HIGH] CVE-2026-32286 · pgproto3 DoS via Negative DataRow Field Length
243+
244+
| Field | Value |
245+
|--------------|-------|
246+
| **ID** | CVE-2026-32286 |
247+
| **Severity** | High · 7.5 |
248+
| **Status** | Awaiting Upstream |
249+
250+
**What**
251+
`github.com/jackc/pgproto3/v2` v2.3.3 — `DataRow.Decode` does not validate field length
252+
before allocation. A malicious or compromised PostgreSQL server can send a DataRow message
253+
with a negative field length to trigger a panic or excessive memory allocation, causing denial
254+
of service (CWE-400).
255+
256+
**Who**
257+
258+
- Discovered by: Automated scan (Trivy image scan)
259+
- Reported: 2026-05-20
260+
- Affects: Charon deployments connecting to an untrusted PostgreSQL server (non-default)
261+
262+
**Where**
263+
264+
- Component: `github.com/jackc/pgproto3/v2` v2.3.3 (transitive Go dependency)
265+
- Versions affected: pgproto3/v2 < patched version
266+
267+
**When**
268+
269+
- Discovered: 2026-05-20
270+
- Disclosed (if public): Public
271+
- Target fix: When upstream publishes a patched release
272+
273+
**How**
274+
Exploitation requires a malicious or compromised PostgreSQL server to send a crafted DataRow
275+
message. Charon's default installation uses SQLite and does not connect to PostgreSQL. Users
276+
deploying Charon with a PostgreSQL backend (non-standard configuration) may be exposed if the
277+
database server is untrusted. EPSS score not yet available.
278+
279+
**Planned Remediation**
280+
Monitor https://github.com/jackc/pgproto3 for a fix release. Upgrade the indirect dependency
281+
once a patched version is available. Pre-existing; not introduced by PR #1031.
282+
198283
## Patched Vulnerabilities
199284

200285
### [HIGH] CVE-2026-34040 · Docker AuthZ Plugin Bypass via Oversized Request Body
@@ -756,4 +841,4 @@ We recognize security researchers who help improve Charon:
756841

757842
---
758843

759-
**Last Updated**: 2026-03-24
844+
**Last Updated**: 2026-05-18

backend/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/hashicorp/yamux v0.1.2
1313
github.com/mattn/go-sqlite3 v1.14.44
1414
github.com/moby/moby/client v0.4.1
15-
github.com/oschwald/geoip2-golang/v2 v2.1.0
15+
github.com/oschwald/geoip2-golang/v2 v2.2.0
1616
github.com/prometheus/client_golang v1.23.2
1717
github.com/robfig/cron/v3 v3.0.1
1818
github.com/sirupsen/logrus v1.9.4
@@ -69,7 +69,7 @@ require (
6969
github.com/ncruces/go-strftime v1.0.0 // indirect
7070
github.com/opencontainers/go-digest v1.0.0 // indirect
7171
github.com/opencontainers/image-spec v1.1.1 // indirect
72-
github.com/oschwald/maxminddb-golang/v2 v2.2.0 // indirect
72+
github.com/oschwald/maxminddb-golang/v2 v2.3.0 // indirect
7373
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
7474
github.com/pmezard/go-difflib v1.0.0 // indirect
7575
github.com/prometheus/client_model v0.6.2 // indirect

backend/go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
114114
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
115115
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
116116
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
117-
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
118-
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
119-
github.com/oschwald/maxminddb-golang/v2 v2.2.0 h1:/2khmIiNvFxgfwGxitper3XBJBs5qTCPQ/H1iR9MgBw=
120-
github.com/oschwald/maxminddb-golang/v2 v2.2.0/go.mod h1:n/ctYVTFYQypkn5uO1CZnTmj8jdQKIVh/LX7gSaIl0w=
117+
github.com/oschwald/geoip2-golang/v2 v2.2.0 h1:gdkhpnHQMiH9ymOI+zSB0QKFGH+n4TntNt7vz+TxGPY=
118+
github.com/oschwald/geoip2-golang/v2 v2.2.0/go.mod h1:xW4tCeQiNU1gqMD1x7zEH2CDNM3d796Ls50yxYDaX0U=
119+
github.com/oschwald/maxminddb-golang/v2 v2.3.0 h1:PnXjMGjkSQlwOBSyZ7hk6Fd75t7erkAhJNJgEhA3MQU=
120+
github.com/oschwald/maxminddb-golang/v2 v2.3.0/go.mod h1:NSQvgFwPxODpBTJI5+5Ns1AAucnx7ggW9PSRRifAT1s=
121121
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
122122
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
123123
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

backend/internal/api/handlers/crowdsec_coverage_target_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,16 +283,16 @@ func TestRegisterBouncerExecutionFailure(t *testing.T) {
283283

284284
// TestGetAcquisitionConfigFileError tests file read error
285285
func TestGetAcquisitionConfigNotPresent(t *testing.T) {
286-
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", filepath.Join(t.TempDir(), "nonexistent.yaml"))
287-
h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir())
288-
r := gin.New()
289-
g := r.Group("/api/v1")
290-
h.RegisterRoutes(g)
291-
292-
w := httptest.NewRecorder()
293-
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/acquisition", http.NoBody)
294-
r.ServeHTTP(w, req)
295-
296-
// File won't exist in test env, should give 404
297-
require.Equal(t, http.StatusNotFound, w.Code)
286+
t.Setenv("CHARON_CROWDSEC_ACQUIS_PATH", filepath.Join(t.TempDir(), "nonexistent.yaml"))
287+
h := newTestCrowdsecHandler(t, OpenTestDB(t), &fakeExec{}, "/bin/false", t.TempDir())
288+
r := gin.New()
289+
g := r.Group("/api/v1")
290+
h.RegisterRoutes(g)
291+
292+
w := httptest.NewRecorder()
293+
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crowdsec/acquisition", http.NoBody)
294+
r.ServeHTTP(w, req)
295+
296+
// File won't exist in test env, should give 404
297+
require.Equal(t, http.StatusNotFound, w.Code)
298298
}

backend/internal/api/handlers/crowdsec_stop_lapi_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,10 @@ func TestGetLAPIDecisions_WithMockServer(t *testing.T) {
167167

168168
secSvc := createTestSecurityService(t, db)
169169
h := &CrowdsecHandler{
170-
DB: db,
171-
Security: secSvc,
172-
CmdExec: &mockCommandExecutor{},
173-
DataDir: t.TempDir(),
170+
DB: db,
171+
Security: secSvc,
172+
CmdExec: &mockCommandExecutor{},
173+
DataDir: t.TempDir(),
174174
validateLAPIURL: permissiveLAPIURLValidator,
175175
}
176176

@@ -210,10 +210,10 @@ func TestGetLAPIDecisions_Unauthorized(t *testing.T) {
210210

211211
secSvc := createTestSecurityService(t, db)
212212
h := &CrowdsecHandler{
213-
DB: db,
214-
Security: secSvc,
215-
CmdExec: &mockCommandExecutor{},
216-
DataDir: t.TempDir(),
213+
DB: db,
214+
Security: secSvc,
215+
CmdExec: &mockCommandExecutor{},
216+
DataDir: t.TempDir(),
217217
validateLAPIURL: permissiveLAPIURLValidator,
218218
}
219219

@@ -245,10 +245,10 @@ func TestGetLAPIDecisions_NullResponse(t *testing.T) {
245245

246246
secSvc := createTestSecurityService(t, db)
247247
h := &CrowdsecHandler{
248-
DB: db,
249-
Security: secSvc,
250-
CmdExec: &mockCommandExecutor{},
251-
DataDir: t.TempDir(),
248+
DB: db,
249+
Security: secSvc,
250+
CmdExec: &mockCommandExecutor{},
251+
DataDir: t.TempDir(),
252252
validateLAPIURL: permissiveLAPIURLValidator,
253253
}
254254

@@ -323,10 +323,10 @@ func TestCheckLAPIHealth_WithMockServer(t *testing.T) {
323323

324324
secSvc := createTestSecurityService(t, db)
325325
h := &CrowdsecHandler{
326-
DB: db,
327-
Security: secSvc,
328-
CmdExec: &mockCommandExecutor{},
329-
DataDir: t.TempDir(),
326+
DB: db,
327+
Security: secSvc,
328+
CmdExec: &mockCommandExecutor{},
329+
DataDir: t.TempDir(),
330330
validateLAPIURL: permissiveLAPIURLValidator,
331331
}
332332

@@ -369,10 +369,10 @@ func TestCheckLAPIHealth_FallbackToDecisions(t *testing.T) {
369369

370370
secSvc := createTestSecurityService(t, db)
371371
h := &CrowdsecHandler{
372-
DB: db,
373-
Security: secSvc,
374-
CmdExec: &mockCommandExecutor{},
375-
DataDir: t.TempDir(),
372+
DB: db,
373+
Security: secSvc,
374+
CmdExec: &mockCommandExecutor{},
375+
DataDir: t.TempDir(),
376376
validateLAPIURL: permissiveLAPIURLValidator,
377377
}
378378

backend/internal/api/handlers/crowdsec_wave5_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ func TestCrowdsecWave5_GetLAPIDecisions_Unauthorized(t *testing.T) {
3535
}))
3636
t.Cleanup(server.Close)
3737

38-
3938
require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error)
4039

4140
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)
@@ -63,7 +62,6 @@ func TestCrowdsecWave5_GetLAPIDecisions_NonJSONContentTypeFallsBack(t *testing.T
6362
}))
6463
t.Cleanup(server.Close)
6564

66-
6765
require.NoError(t, db.Create(&models.SecurityConfig{UUID: "default", CrowdSecAPIURL: server.URL}).Error)
6866

6967
h := newTestCrowdsecHandler(t, db, &fakeExec{}, "/bin/false", tmpDir)

0 commit comments

Comments
 (0)