mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-05-21 13:46:05 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8480175eac | |||
| c082cd89dc | |||
| 03bf695dff | |||
| 51ae340a7b | |||
| 430e262c84 | |||
| a47efe31b0 | |||
| 763c896b63 | |||
| a426230698 | |||
| 6c3fc188fb | |||
| a0589d3c7a | |||
| b57508afcd | |||
| 276b537776 |
@@ -39,3 +39,5 @@ wenet
|
||||
qwertiko
|
||||
setuplistener
|
||||
mba
|
||||
xfu
|
||||
xou
|
||||
|
||||
@@ -27,6 +27,7 @@ jobs:
|
||||
- palemoon/amd64
|
||||
#- palemoon/i386
|
||||
- robots_txt
|
||||
- traefik
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: tekton.dev/v1
|
||||
kind: PipelineRun
|
||||
metadata:
|
||||
generateName: anubis-m-
|
||||
namespace: ci
|
||||
|
||||
spec:
|
||||
params:
|
||||
- name: commit
|
||||
value: "Xe/tekton"
|
||||
- name: branch
|
||||
value: main
|
||||
pipelineRef:
|
||||
name: anubis-build-test
|
||||
taskRunTemplate:
|
||||
serviceAccountName: anubis-k3k
|
||||
timeouts:
|
||||
pipeline: 1h0m0s
|
||||
workspaces:
|
||||
- name: repo
|
||||
volumeClaimTemplate:
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 4Gi
|
||||
- name: go-mod-cache
|
||||
persistentVolumeClaim:
|
||||
claimName: go-mod-cache
|
||||
- name: dockerconfig-atcr
|
||||
secret:
|
||||
secretName: atcr
|
||||
- name: dockerconfig-ghcr
|
||||
secret:
|
||||
secretName: ghcr
|
||||
@@ -0,0 +1,217 @@
|
||||
apiVersion: tekton.dev/v1beta1
|
||||
kind: Pipeline
|
||||
metadata:
|
||||
name: anubis-build-test
|
||||
namespace: ci
|
||||
|
||||
spec:
|
||||
description: |
|
||||
The CI/CD pipeline for Anubis
|
||||
params:
|
||||
- name: repo-url
|
||||
type: string
|
||||
description: "Git repo to clone"
|
||||
default: "https://github.com/TecharoHQ/anubis"
|
||||
- name: "branch"
|
||||
type: string
|
||||
description: "Git branch to operate against"
|
||||
- name: "commit"
|
||||
type: string
|
||||
description: "Git revision to check out"
|
||||
- name: "actor"
|
||||
type: string
|
||||
description: "Tangled actor"
|
||||
default: "did:web:anubis.techaro.lol"
|
||||
- name: docker-image-base
|
||||
type: string
|
||||
description: string prefix for production docker images
|
||||
default: "registry.int.xeserv.us/techarohq"
|
||||
- name: docker-cache
|
||||
type: string
|
||||
description: docker repo to store cache files
|
||||
default: "registry.int.xeserv.us/techarohq/anubis/cache"
|
||||
- name: go-version
|
||||
type: string
|
||||
description: "Go version to use"
|
||||
default: "1.26.3"
|
||||
workspaces:
|
||||
- name: repo
|
||||
description: |
|
||||
Cloned repo files.
|
||||
- name: dockerconfig-atcr
|
||||
description: |
|
||||
Docker config for pushing images to atcr
|
||||
- name: dockerconfig-ghcr
|
||||
description: |
|
||||
Docker config for pushing images to ghcr
|
||||
tasks:
|
||||
- name: fix-permissions
|
||||
taskRef:
|
||||
name: fix-permissions
|
||||
workspaces:
|
||||
- name: dir
|
||||
workspace: repo
|
||||
- name: clone-repo
|
||||
runAfter: ["fix-permissions"]
|
||||
taskRef:
|
||||
name: git-clone-naive
|
||||
workspaces:
|
||||
- name: output
|
||||
workspace: repo
|
||||
params:
|
||||
- name: url
|
||||
value: $(params.repo-url)
|
||||
- name: revision
|
||||
value: $(params.commit)
|
||||
- name: docker-build-ci
|
||||
runAfter: ["clone-repo"]
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: repo
|
||||
taskRef:
|
||||
name: kaniko
|
||||
params:
|
||||
- name: IMAGE
|
||||
value: $(params.docker-image-base)/anubis/ci:$(tasks.clone-repo.results.version)
|
||||
- name: DOCKERFILE
|
||||
value: ./test/ssh-ci/Dockerfile
|
||||
- name: EXTRA_ARGS
|
||||
value:
|
||||
[
|
||||
"--build-arg=GO_VERSION=$(params.go-version)",
|
||||
"--cache",
|
||||
"--cache-copy-layers",
|
||||
"--cache-run-layers",
|
||||
"--cache-repo=$(params.docker-cache)",
|
||||
"--label=org.tangled.actor=$(params.actor)",
|
||||
"--snapshot-mode=redo",
|
||||
"--use-new-run",
|
||||
]
|
||||
- name: provision-test-cluster
|
||||
runAfter: ["docker-build-ci"]
|
||||
taskSpec:
|
||||
workspaces:
|
||||
- name: repo
|
||||
mountPath: /src
|
||||
results:
|
||||
- name: cluster-name
|
||||
description: "k3k cluster name object in k8s"
|
||||
steps:
|
||||
- name: create-cluster
|
||||
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||
workingDir: $(workspaces.repo.path)/repo
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
value: $(context.pipelineRun.namespace)
|
||||
- name: PIPELINE_NAME
|
||||
value: $(context.pipeline.name)
|
||||
- name: PIPELINERUN_NAME
|
||||
value: $(context.pipelineRun.name)
|
||||
- name: PIPELINERUN_UID
|
||||
value: $(context.pipelineRun.uid)
|
||||
- name: KUBECONFIG_OUT
|
||||
value: $(workspaces.repo.path)/kube/config
|
||||
script: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
./test/k3k/create-cluster.sh > "$(results.cluster-name.path)"
|
||||
- name: build-assets
|
||||
runAfter: ["docker-build-ci"]
|
||||
taskSpec:
|
||||
workspaces:
|
||||
- name: repo
|
||||
mountPath: /src
|
||||
steps:
|
||||
- name: test
|
||||
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||
workingDir: $(workspaces.repo.path)/repo
|
||||
script: |
|
||||
npm ci
|
||||
npm run assets
|
||||
workspaces:
|
||||
- name: repo
|
||||
workspace: repo
|
||||
- name: go-test
|
||||
runAfter: ["build-assets"]
|
||||
taskSpec:
|
||||
workspaces:
|
||||
- name: repo
|
||||
mountPath: /src
|
||||
steps:
|
||||
- name: test
|
||||
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||
workingDir: $(workspaces.repo.path)/repo
|
||||
script: |
|
||||
SKIP_INTEGRATION=1 go test ./...
|
||||
workspaces:
|
||||
- name: repo
|
||||
workspace: repo
|
||||
- name: test-anubis
|
||||
runAfter: ["build-assets"]
|
||||
taskRef:
|
||||
name: ko
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: repo
|
||||
params:
|
||||
- name: VERSION
|
||||
value: $(tasks.clone-repo.results.version)
|
||||
- name: SOURCE_DATE_EPOCH
|
||||
value: $(tasks.clone-repo.results.source-date-epoch)
|
||||
- name: KO_DOCKER_REPO
|
||||
value: $(params.docker-image-base)
|
||||
- name: extra-args
|
||||
value:
|
||||
[
|
||||
"--platform=all",
|
||||
"--base-import-paths",
|
||||
"--tags=$(tasks.clone-repo.results.version)",
|
||||
"--image-label=org.tangled.actor=$(params.actor)",
|
||||
]
|
||||
- name: packages
|
||||
value:
|
||||
- ./cmd/anubis
|
||||
- name: integration
|
||||
runAfter:
|
||||
- "provision-test-cluster"
|
||||
- "build-assets"
|
||||
- "test-anubis"
|
||||
matrix:
|
||||
params:
|
||||
- name: test-case
|
||||
value:
|
||||
- default-config-macro
|
||||
- i18n
|
||||
- robots_txt
|
||||
taskSpec:
|
||||
params:
|
||||
- name: test-case
|
||||
type: string
|
||||
workspaces:
|
||||
- name: repo
|
||||
mountPath: /src
|
||||
steps:
|
||||
- name: exec
|
||||
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||
workingDir: $(workspaces.repo.path)/repo/test/$(params.test-case)
|
||||
script: |
|
||||
./tekton.sh
|
||||
env:
|
||||
- name: KUBECONFIG
|
||||
value: "$(workspaces.repo.path)/kube/config"
|
||||
finally:
|
||||
- name: teardown-cluster
|
||||
when:
|
||||
- input: "$(tasks.provision-test-cluster.status)"
|
||||
operator: in
|
||||
values: ["Succeeded"]
|
||||
taskSpec:
|
||||
workspaces:
|
||||
- name: repo
|
||||
mountPath: /src
|
||||
steps:
|
||||
- name: delete
|
||||
image: $(tasks.docker-build-ci.results.IMAGE_URL)@$(tasks.docker-build-ci.results.IMAGE_DIGEST)
|
||||
workingDir: $(workspaces.repo.path)/repo
|
||||
script: |
|
||||
kubectl delete --ignore-not-found -n $(context.pipelineRun.namespace) clusters.k3k.io/"$(tasks.provision-test-cluster.results.cluster-name)"
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace: ci
|
||||
resources:
|
||||
- anubis-test.yaml
|
||||
- rbac.yaml
|
||||
@@ -0,0 +1,32 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: anubis-k3k
|
||||
namespace: ci
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: anubis-k3k
|
||||
namespace: ci
|
||||
rules:
|
||||
- apiGroups: ["k3k.io"]
|
||||
resources: ["clusters"]
|
||||
verbs: ["*"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: anubis-k3k
|
||||
namespace: ci
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: anubis-k3k
|
||||
namespace: ci
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: anubis-k3k
|
||||
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
<!-- This changes the project to: -->
|
||||
|
||||
- Patch [GHSA-6wcg-mqvh-fcvg](https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg) by containing subrequest logic to Anubis instances in subrequest mode.
|
||||
- Implement robot9001 style delays on the honeypot feature so that the first hit takes 1 millisecond, the second takes 2, etc.
|
||||
- Move metrics server configuration to [the policy file](./admin/policies.mdx#metrics-server).
|
||||
- Expose [pprof endpoints](https://pkg.go.dev/net/http/pprof) on the metrics listener to enable profiling Anubis in production.
|
||||
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
||||
@@ -28,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
||||
- Add config option to add ASN to logs/metrics.
|
||||
- Log weight when issuing challenge
|
||||
- Fix `path_regex` and CEL `path` rules not matching when using Traefik `forwardAuth` middleware. Anubis now checks `X-Forwarded-Uri` (Traefik) in addition to `X-Original-URI` (nginx) when resolving the request path in subrequest mode ([#1628](https://github.com/TecharoHQ/anubis/issues/1628)).
|
||||
|
||||
## v1.25.0: Necron
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -168,6 +169,9 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
millisecondAmount := math.Pow(float64(networkCount), 2)
|
||||
time.Sleep(time.Duration(millisecondAmount) * time.Millisecond)
|
||||
|
||||
spins := i.makeSpins()
|
||||
affirmations := i.makeAffirmations()
|
||||
title := i.makeTitle()
|
||||
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type CELChecker struct {
|
||||
program cel.Program
|
||||
src string
|
||||
program cel.Program
|
||||
src string
|
||||
subRequestMode bool
|
||||
}
|
||||
|
||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
|
||||
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns, subRequestMode bool) (*CELChecker, error) {
|
||||
env, err := expressions.BotEnvironment(dnsObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -29,8 +30,9 @@ func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker,
|
||||
}
|
||||
|
||||
return &CELChecker{
|
||||
src: cfg.String(),
|
||||
program: program,
|
||||
src: cfg.String(),
|
||||
program: program,
|
||||
subRequestMode: subRequestMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -39,7 +41,7 @@ func (cc *CELChecker) Hash() string {
|
||||
}
|
||||
|
||||
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
||||
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r})
|
||||
result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r, cc.subRequestMode})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -54,6 +56,7 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
||||
|
||||
type CELRequest struct {
|
||||
*http.Request
|
||||
subRequestMode bool
|
||||
}
|
||||
|
||||
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
||||
@@ -71,6 +74,14 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
||||
case "userAgent":
|
||||
return cr.UserAgent(), true
|
||||
case "path":
|
||||
if cr.subRequestMode {
|
||||
if xou := cr.Header.Get("X-Original-URI"); xou != "" {
|
||||
return xou, true
|
||||
}
|
||||
if xfu := cr.Header.Get("X-Forwarded-Uri"); xfu != "" {
|
||||
return xfu, true
|
||||
}
|
||||
}
|
||||
return cr.URL.Path, true
|
||||
case "query":
|
||||
return expressions.URLValues{Values: cr.URL.Query()}, true
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
||||
Expression: `headers.exists(k, k == "Accept") && query.exists(k, k == "format")`,
|
||||
}
|
||||
|
||||
checker, err := NewCELChecker(cfg, newTestDNS(t))
|
||||
checker, err := NewCELChecker(cfg, newTestDNS(t), false)
|
||||
if err != nil {
|
||||
t.Fatalf("creating CEL checker failed: %v", err)
|
||||
}
|
||||
@@ -42,3 +42,77 @@ func TestCELChecker_MapIterationWrappers(t *testing.T) {
|
||||
t.Fatal("expected expression to evaluate true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCELChecker_PathWithForwardedUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
xForwardedUri string
|
||||
urlPath string
|
||||
subRequestMode bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "path matches X-Forwarded-Uri in subrequest mode",
|
||||
expression: `path.startsWith("/admin")`,
|
||||
xForwardedUri: "/admin/secret",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "path with query string",
|
||||
expression: `path.startsWith("/api/secret")`,
|
||||
xForwardedUri: "/api/secret?token=abc",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "path falls back to url path when no header",
|
||||
expression: `path == "/public/page"`,
|
||||
urlPath: "/public/page",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||
expression: `path.startsWith("/admin")`,
|
||||
xForwardedUri: "/admin/secret",
|
||||
urlPath: "/public/page",
|
||||
subRequestMode: false,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &config.ExpressionOrList{
|
||||
Expression: tt.expression,
|
||||
}
|
||||
checker, err := NewCELChecker(cfg, newTestDNS(t), tt.subRequestMode)
|
||||
if err != nil {
|
||||
t.Fatalf("NewCELChecker() error: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("http.NewRequest: %v", err)
|
||||
}
|
||||
|
||||
if tt.xForwardedUri != "" {
|
||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||
}
|
||||
|
||||
got, err := checker.Check(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Check() error: %v", err)
|
||||
}
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q)",
|
||||
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,9 @@ func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) {
|
||||
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
||||
if pc.subRequestMode {
|
||||
originalUrl := r.Header.Get("X-Original-URI")
|
||||
if originalUrl == "" {
|
||||
originalUrl = r.Header.Get("X-Forwarded-Uri")
|
||||
}
|
||||
if originalUrl != "" {
|
||||
if pc.regexp.MatchString(originalUrl) {
|
||||
return true, nil
|
||||
|
||||
@@ -410,3 +410,119 @@ func TestPathChecker_GHSA_6wcg_mqvh_fcvg(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathChecker_XForwardedUri(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
regex string
|
||||
xForwardedUri string
|
||||
xOriginalURI string
|
||||
urlPath string
|
||||
subRequestMode bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "X-Forwarded-Uri matches regex in subrequest mode",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/admin/users",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "X-Forwarded-Uri with query string",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/admin/users?page=1",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "X-Original-URI takes priority over X-Forwarded-Uri",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/public/page",
|
||||
xOriginalURI: "/admin/users",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "falls back to X-Forwarded-Uri when no X-Original-URI",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/admin/dashboard",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "neither header matches, url path matches",
|
||||
regex: "^/public/.*",
|
||||
xForwardedUri: "/admin/users",
|
||||
urlPath: "/public/page",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nothing matches",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/public/page",
|
||||
urlPath: "/.within.website/x/cmd/anubis/api/check",
|
||||
subRequestMode: true,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non-subrequest mode ignores X-Forwarded-Uri",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/admin/users",
|
||||
urlPath: "/public/page",
|
||||
subRequestMode: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "non-subrequest mode uses url path",
|
||||
regex: "^/admin/.*",
|
||||
xForwardedUri: "/public/page",
|
||||
urlPath: "/admin/secret",
|
||||
subRequestMode: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "empty X-Forwarded-Uri falls back to url path",
|
||||
regex: "^/check$",
|
||||
urlPath: "/check",
|
||||
subRequestMode: true,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pc, err := NewPathChecker(tt.regex, tt.subRequestMode)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPathChecker(%q, %v) returned error: %v", tt.regex, tt.subRequestMode, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://example.com"+tt.urlPath, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("http.NewRequest: %v", err)
|
||||
}
|
||||
|
||||
if tt.xForwardedUri != "" {
|
||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||
}
|
||||
if tt.xOriginalURI != "" {
|
||||
req.Header.Set("X-Original-URI", tt.xOriginalURI)
|
||||
}
|
||||
|
||||
got, err := pc.Check(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Check() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("Check() = %v, want %v (subRequestMode=%v, urlPath=%q, X-Forwarded-Uri=%q, X-Original-URI=%q)",
|
||||
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xForwardedUri, tt.xOriginalURI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
||||
}
|
||||
|
||||
if b.Expression != nil {
|
||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
||||
c, err := NewCELChecker(b.Expression, result.Dns, subrequestMode)
|
||||
if err != nil {
|
||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||
} else {
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@
|
||||
"package": "go tool yeet",
|
||||
"lint": "make lint",
|
||||
"prepare": "husky && go mod download",
|
||||
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w ."
|
||||
"format": "prettier -w . 2>&1 >/dev/null && go run goimports -w .",
|
||||
"deploy:ci": "kubectl apply -k .tekton -n ci --context admin@alrest",
|
||||
"deploy:ci:invoke": "npm run deploy:ci && kubectl create -f .tekton/anubis-pipelinerun.yaml -n ci --context admin@alrest"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
exec ./test.sh
|
||||
@@ -3,5 +3,10 @@
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
python3 -m venv .env
|
||||
source .env/bin/activate
|
||||
pip install pyyaml
|
||||
|
||||
python3 -c 'import yaml'
|
||||
python3 ./compare_bots.py
|
||||
python3 ./compare_bots.py
|
||||
|
||||
@@ -104,5 +104,6 @@ require (
|
||||
|
||||
tool (
|
||||
github.com/TecharoHQ/anubis/cmd/anubis
|
||||
github.com/TecharoHQ/anubis/utils/cmd/backoff-retry
|
||||
github.com/jsha/minica
|
||||
)
|
||||
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
function cleanup() {
|
||||
pkill -P $$
|
||||
}
|
||||
|
||||
trap cleanup EXIT SIGINT
|
||||
|
||||
go tool anubis --help 2>/dev/null || :
|
||||
|
||||
go run ../cmd/unixhttpd &
|
||||
|
||||
go tool anubis \
|
||||
--policy-fname ./anubis.yaml \
|
||||
--use-remote-address \
|
||||
--target=unix://$(pwd)/unixhttpd.sock &
|
||||
|
||||
go tool backoff-retry node ./test.mjs
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a k3k cluster, wait for it to be Ready, and write its kubeconfig.
|
||||
# Prints the generated cluster name to stdout on success.
|
||||
#
|
||||
# Required env:
|
||||
# NAMESPACE Kubernetes namespace to create the cluster in
|
||||
# KUBECONFIG_OUT Path to write the resulting kubeconfig
|
||||
#
|
||||
# Optional env (set under Tekton to enable ownerReference-based GC + labels):
|
||||
# PIPELINE_NAME Tekton Pipeline name
|
||||
# PIPELINERUN_NAME Tekton PipelineRun name
|
||||
# PIPELINERUN_UID Tekton PipelineRun UID
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${NAMESPACE:?NAMESPACE must be set}"
|
||||
: "${KUBECONFIG_OUT:?KUBECONFIG_OUT must be set}"
|
||||
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
cluster_name=$(kubectl create -n "${NAMESPACE}" -f "${script_dir}/test-cluster.yaml" -ojson | jq -r '.metadata.name')
|
||||
|
||||
if [[ -n "${PIPELINERUN_NAME:-}" && -n "${PIPELINERUN_UID:-}" ]]; then
|
||||
owner_ref=$(jo \
|
||||
apiVersion=tekton.dev/v1 \
|
||||
kind=PipelineRun \
|
||||
name="${PIPELINERUN_NAME}" \
|
||||
uid="${PIPELINERUN_UID}" \
|
||||
blockOwnerDeletion=false)
|
||||
patch=$(jo metadata=$(jo "ownerReferences[]=${owner_ref}"))
|
||||
|
||||
kubectl patch -n "${NAMESPACE}" "clusters.k3k.io/${cluster_name}" --type=merge -p "${patch}" >&2
|
||||
|
||||
kubectl label -n "${NAMESPACE}" "clusters.k3k.io/${cluster_name}" \
|
||||
"tekton.dev/memberOf=tasks" \
|
||||
"tekton.dev/pipeline=${PIPELINE_NAME:-}" \
|
||||
"tekton.dev/pipelineRun=${PIPELINERUN_NAME}" \
|
||||
"tekton.dev/pipelineRunUID=${PIPELINERUN_UID}" >&2
|
||||
fi
|
||||
|
||||
kubectl wait --for=condition=Ready "clusters.k3k.io/${cluster_name}" -n "${NAMESPACE}" --timeout 5m >&2
|
||||
kubectl wait --for=create "secret/k3k-${cluster_name}-kubeconfig" -n "${NAMESPACE}" --timeout 5m >&2
|
||||
|
||||
mkdir -p "$(dirname "${KUBECONFIG_OUT}")"
|
||||
kubectl get -ojson -n "${NAMESPACE}" "secret/k3k-${cluster_name}-kubeconfig" \
|
||||
| jq -r '.data["kubeconfig.yaml"]' \
|
||||
| base64 -d > "${KUBECONFIG_OUT}"
|
||||
|
||||
echo "${cluster_name}"
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: k3k.io/v1beta1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
generateName: anubis-test-
|
||||
namespace: ci
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
function cleanup() {
|
||||
pkill -P $$
|
||||
}
|
||||
|
||||
trap cleanup EXIT SIGINT
|
||||
|
||||
go tool anubis --help 2>/dev/null || :
|
||||
|
||||
go run ../cmd/unixhttpd &
|
||||
|
||||
go tool anubis \
|
||||
--policy-fname ./anubis.yaml \
|
||||
--use-remote-address \
|
||||
--serve-robots-txt \
|
||||
--target=unix://$(pwd)/unixhttpd.sock &
|
||||
|
||||
go tool backoff-retry node ./test.mjs
|
||||
+12
-2
@@ -1,5 +1,15 @@
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG GO_VERSION=1.26.3
|
||||
|
||||
# Go toolchain bootstrapper
|
||||
FROM golang:${GO_VERSION} AS go
|
||||
|
||||
RUN CGO_ENABLED=0 go install golang.org/dl/go1.23.6@latest \
|
||||
&& mkdir -p /app/bin \
|
||||
&& mv /go/bin/go1.23.6 /app/bin/go
|
||||
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
RUN apk add -U go nodejs git build-base git npm bash zstd brotli gzip
|
||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||
COPY --from=go /app/bin/go /usr/local/bin/go
|
||||
|
||||
RUN apk add -U nodejs git build-base git npm bash zstd brotli gzip jq jo kubectl python3 py3-pip py3-virtualenv \
|
||||
&& go download
|
||||
@@ -0,0 +1,16 @@
|
||||
bots:
|
||||
- name: block-admin-via-regex
|
||||
path_regex: ^/admin(/.*)?$
|
||||
action: DENY
|
||||
|
||||
- name: block-secret-via-cel
|
||||
expression:
|
||||
all:
|
||||
- 'path.startsWith("/api/secret")'
|
||||
action: DENY
|
||||
|
||||
- import: (data)/meta/default-config.yaml
|
||||
|
||||
status_codes:
|
||||
CHALLENGE: 200
|
||||
DENY: 403
|
||||
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.3
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
volumes:
|
||||
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./http.yaml:/config/http.yaml:ro
|
||||
|
||||
anubis:
|
||||
image: ko.local/anubis
|
||||
restart: always
|
||||
environment:
|
||||
BIND: ":8080"
|
||||
TARGET: " "
|
||||
POLICY_FNAME: /etc/techaro/anubis.yaml
|
||||
PUBLIC_URL: http://localhost:8080/.within.website/x/cmd/anubis
|
||||
COOKIE_DOMAIN: localhost
|
||||
USE_REMOTE_ADDRESS: "true"
|
||||
volumes:
|
||||
- ./anubis.yaml:/etc/techaro/anubis.yaml
|
||||
|
||||
backend:
|
||||
image: ghcr.io/xe/x/httpdebug
|
||||
pull_policy: always
|
||||
restart: always
|
||||
@@ -0,0 +1,30 @@
|
||||
http:
|
||||
middlewares:
|
||||
anubis:
|
||||
forwardAuth:
|
||||
address: http://anubis:8080/.within.website/x/cmd/anubis/api/check
|
||||
trustForwardHeader: true
|
||||
|
||||
routers:
|
||||
anubis-assets:
|
||||
rule: Host(`localhost`) && PathPrefix(`/.within.website/x/cmd/anubis`)
|
||||
entryPoints:
|
||||
- web
|
||||
service: anubis
|
||||
backend:
|
||||
rule: Host(`localhost`)
|
||||
entryPoints:
|
||||
- web
|
||||
service: backend
|
||||
middlewares:
|
||||
- anubis
|
||||
|
||||
services:
|
||||
anubis:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://anubis:8080
|
||||
backend:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://backend:3000
|
||||
@@ -0,0 +1,33 @@
|
||||
// Smoke test for https://github.com/TecharoHQ/anubis/issues/1628
|
||||
//
|
||||
// Traefik's forwardAuth middleware calls Anubis at the literal path
|
||||
// /.within.website/x/cmd/anubis/api/check and conveys the original URL in the
|
||||
// X-Forwarded-Uri header. Path-targeting policy rules must match that header
|
||||
// (not r.URL.Path), otherwise every request looks like a request to /check.
|
||||
|
||||
const BASE = "http://localhost:8080";
|
||||
const UA = "Mozilla/5.0 (compatible; AnubisTraefikSmoke/1.0)";
|
||||
|
||||
const cases = [
|
||||
{ path: "/", expected: 307, why: "control: no DENY rule, default challenge redirect" },
|
||||
{ path: "/free", expected: 307, why: "control: no DENY rule, default challenge redirect" },
|
||||
{ path: "/admin", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
|
||||
{ path: "/admin/users", expected: 403, why: "path_regex must match X-Forwarded-Uri, not 307 or 200" },
|
||||
{ path: "/api/secret", expected: 403, why: "CEL path must match X-Forwarded-Uri, not 307 or 200" },
|
||||
];
|
||||
|
||||
let failed = false;
|
||||
|
||||
for (const c of cases) {
|
||||
const resp = await fetch(`${BASE}${c.path}`, {
|
||||
headers: { "User-Agent": UA },
|
||||
redirect: "manual",
|
||||
});
|
||||
const ok = resp.status === c.expected;
|
||||
console.log(
|
||||
`${ok ? "PASS" : "FAIL"}: GET ${c.path} → ${resp.status} (want ${c.expected}: ${c.why})`,
|
||||
);
|
||||
if (!ok) failed = true;
|
||||
}
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
export VERSION=${GITHUB_SHA:-devel}-test
|
||||
export KO_DOCKER_REPO=ko.local
|
||||
|
||||
set -u
|
||||
|
||||
source ../lib/lib.sh
|
||||
|
||||
build_anubis_ko
|
||||
|
||||
function cleanup() {
|
||||
docker compose down -t 1 || :
|
||||
}
|
||||
|
||||
trap cleanup EXIT SIGINT
|
||||
|
||||
docker compose up -d
|
||||
|
||||
backoff-retry --try-count 20 node ./test.mjs
|
||||
@@ -0,0 +1,8 @@
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
|
||||
providers:
|
||||
file:
|
||||
directory: /config
|
||||
watch: false
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Reference in New Issue
Block a user