mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-10 14:28:15 +00:00
Compare commits
18 Commits
4b6475caa4
...
Xe/tekton
| Author | SHA1 | Date | |
|---|---|---|---|
| 8480175eac | |||
| c082cd89dc | |||
| 03bf695dff | |||
| 51ae340a7b | |||
| 430e262c84 | |||
| a47efe31b0 | |||
| 763c896b63 | |||
| a426230698 | |||
| 6c3fc188fb | |||
| a0589d3c7a | |||
| b57508afcd | |||
| 276b537776 | |||
| 9f479f578a | |||
| c184028d42 | |||
| 0491f1fac2 | |||
| d3a00da448 | |||
| 7e037b65e8 | |||
| ebf9a30878 |
@@ -39,3 +39,5 @@ wenet
|
|||||||
qwertiko
|
qwertiko
|
||||||
setuplistener
|
setuplistener
|
||||||
mba
|
mba
|
||||||
|
xfu
|
||||||
|
xou
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ fahedouch
|
|||||||
fastcgi
|
fastcgi
|
||||||
FCr
|
FCr
|
||||||
fcrdns
|
fcrdns
|
||||||
|
fcvg
|
||||||
fediverse
|
fediverse
|
||||||
ffprobe
|
ffprobe
|
||||||
fhdr
|
fhdr
|
||||||
@@ -238,6 +239,7 @@ mnt
|
|||||||
Mojeek
|
Mojeek
|
||||||
mojeekbot
|
mojeekbot
|
||||||
mozilla
|
mozilla
|
||||||
|
mqvh
|
||||||
myclient
|
myclient
|
||||||
mymaster
|
mymaster
|
||||||
mypass
|
mypass
|
||||||
@@ -387,6 +389,7 @@ vnd
|
|||||||
VPS
|
VPS
|
||||||
Vultr
|
Vultr
|
||||||
WAIFU
|
WAIFU
|
||||||
|
wcg
|
||||||
weblate
|
weblate
|
||||||
webmaster
|
webmaster
|
||||||
webpage
|
webpage
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
- palemoon/amd64
|
- palemoon/amd64
|
||||||
#- palemoon/i386
|
#- palemoon/i386
|
||||||
- robots_txt
|
- robots_txt
|
||||||
|
- traefik
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- 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
|
||||||
+1
-1
@@ -259,7 +259,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("loading policy file", "fname", *policyFname)
|
lg.Info("loading policy file", "fname", *policyFname)
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel)
|
policy, err := libanubis.LoadPoliciesOrDefault(ctx, *policyFname, *challengeDifficulty, *slogLevel, strings.TrimSpace(*target) == "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("can't parse policy file: %v", err)
|
log.Fatalf("can't parse policy file: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- 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).
|
- 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.
|
- 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)
|
- fix: prevent nil pointer panic in challenge validation when threshold rules match during PassChallenge (#1463)
|
||||||
@@ -25,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
|
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
|
||||||
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
||||||
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
- 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
|
## v1.25.0: Necron
|
||||||
|
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ Anubis exposes the following logging settings in the policy file:
|
|||||||
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
|
| `level` | [log level](#log-levels) | `info` | The logging level threshold. Any logs that are at or above this threshold will be drained to the sink. Any other logs will be discarded. |
|
||||||
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
|
| `sink` | string | `stdio`, `file` | The sink where the logs drain to as they are being recorded in Anubis. |
|
||||||
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
|
| `parameters` | object | | Parameters for the given logging sink. This will vary based on the logging sink of choice. See below for more information. |
|
||||||
|
| `asn` | bool | `true`, `false` | Add ASN information to logs/metrics. (Requires a Thoth client configured) |
|
||||||
|
|
||||||
Anubis supports the following logging sinks:
|
Anubis supports the following logging sinks:
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
|
|||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"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()
|
spins := i.makeSpins()
|
||||||
affirmations := i.makeAffirmations()
|
affirmations := i.makeAffirmations()
|
||||||
title := i.makeTitle()
|
title := i.makeTitle()
|
||||||
|
|||||||
@@ -595,7 +595,7 @@ func spawnAnubisWithOptions(t *testing.T, basePrefix string) string {
|
|||||||
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
fmt.Fprintf(w, "<html><body><span id=anubis-test>%d</span></body></html>", time.Now().Unix())
|
||||||
})
|
})
|
||||||
|
|
||||||
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info")
|
policy, err := libanubis.LoadPoliciesOrDefault(t.Context(), "", anubis.DefaultDifficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-15
@@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ import (
|
|||||||
"github.com/TecharoHQ/anubis/lib/policy"
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
"github.com/TecharoHQ/anubis/lib/policy/checker"
|
||||||
"github.com/TecharoHQ/anubis/lib/store"
|
"github.com/TecharoHQ/anubis/lib/store"
|
||||||
|
iptoasnv1 "github.com/TecharoHQ/thoth-proto/gen/techaro/thoth/iptoasn/v1"
|
||||||
|
|
||||||
// challenge implementations
|
// challenge implementations
|
||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||||
@@ -39,31 +41,52 @@ import (
|
|||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const asnContextKey contextKey = iota
|
||||||
|
|
||||||
|
type asnInfo struct {
|
||||||
|
ASN string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func asnFromContext(ctx context.Context) (string, string) {
|
||||||
|
if v, ok := ctx.Value(asnContextKey).(asnInfo); ok {
|
||||||
|
return v.ASN, v.Description
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
|
challengesIssued = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_issued",
|
Name: "anubis_challenges_issued",
|
||||||
Help: "The total number of challenges issued",
|
Help: "The total number of challenges issued",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
challengesValidated = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_challenges_validated",
|
Name: "anubis_challenges_validated",
|
||||||
Help: "The total number of challenges validated",
|
Help: "The total number of challenges validated",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
droneBLHits = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_dronebl_hits",
|
Name: "anubis_dronebl_hits",
|
||||||
Help: "The total number of hits from DroneBL",
|
Help: "The total number of hits from DroneBL",
|
||||||
}, []string{"status"})
|
}, []string{"status", "asn", "asn_description"})
|
||||||
|
|
||||||
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
failedValidations = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_failed_validations",
|
Name: "anubis_failed_validations",
|
||||||
Help: "The total number of failed validations",
|
Help: "The total number of failed validations",
|
||||||
}, []string{"method"})
|
}, []string{"method", "asn", "asn_description"})
|
||||||
|
|
||||||
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
requestsProxied = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_proxied_requests_total",
|
Name: "anubis_proxied_requests_total",
|
||||||
Help: "Number of requests proxied through Anubis to upstream targets",
|
Help: "Number of requests proxied through Anubis to upstream targets",
|
||||||
}, []string{"host"})
|
}, []string{"host", "asn", "asn_description"})
|
||||||
|
|
||||||
|
requestsByASN = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: "anubis_requests_by_asn_total",
|
||||||
|
Help: "Number of requests by ASN",
|
||||||
|
}, []string{"asn", "asn_description"})
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -78,6 +101,28 @@ type Server struct {
|
|||||||
hs512Secret []byte
|
hs512Secret []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRequestLogger(r *http.Request) (*slog.Logger, *http.Request) {
|
||||||
|
lg := internal.GetRequestLogger(s.logger, r)
|
||||||
|
|
||||||
|
if s.policy.LogASN && s.policy.ThothClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
|
if info, err := s.policy.ThothClient.IPToASN.Lookup(ctx, &iptoasnv1.LookupRequest{IpAddress: ip}); err == nil && info.GetAnnounced() {
|
||||||
|
asn := strconv.FormatUint(uint64(info.GetAsNumber()), 10)
|
||||||
|
lg = lg.With("asn", info.GetAsNumber(), "asn_description", info.GetDescription())
|
||||||
|
requestsByASN.WithLabelValues(asn, info.GetDescription()).Inc()
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), asnContextKey, asnInfo{
|
||||||
|
ASN: asn,
|
||||||
|
Description: info.GetDescription(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lg, r
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
func (s *Server) getTokenKeyfunc() jwt.Keyfunc {
|
||||||
// return ED25519 key if HS512 is not set
|
// return ED25519 key if HS512 is not set
|
||||||
if len(s.hs512Secret) == 0 {
|
if len(s.hs512Secret) == 0 {
|
||||||
@@ -141,7 +186,7 @@ func (s *Server) issueChallenge(ctx context.Context, r *http.Request, lg *slog.L
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lg.Info("new challenge issued", "challenge", id.String())
|
lg.Info("new challenge issued", "challenge", id.String(), "weight", cr.Weight)
|
||||||
|
|
||||||
return &chall, err
|
return &chall, err
|
||||||
}
|
}
|
||||||
@@ -193,7 +238,7 @@ func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
|
|
||||||
if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
|
if val, _ := s.store.Get(r.Context(), fmt.Sprintf("ogtags:allow:%s%s", r.Host, r.URL.String())); val != nil {
|
||||||
lg.Debug("serving opengraph tag asset")
|
lg.Debug("serving opengraph tag asset")
|
||||||
@@ -218,7 +263,10 @@ func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpS
|
|||||||
r.Header.Add("X-Anubis-Rule", cr.Name)
|
r.Header.Add("X-Anubis-Rule", cr.Name)
|
||||||
r.Header.Add("X-Anubis-Action", string(cr.Rule))
|
r.Header.Add("X-Anubis-Action", string(cr.Rule))
|
||||||
lg = lg.With("check_result", cr)
|
lg = lg.With("check_result", cr)
|
||||||
policy.Applications.WithLabelValues(cr.Name, string(cr.Rule)).Add(1)
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
policy.Applications.WithLabelValues(cr.Name, string(cr.Rule), asn, asnDesc).Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
ip := r.Header.Get("X-Real-Ip")
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
|
|
||||||
@@ -348,7 +396,8 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
|||||||
lg.Error("can't look up ip in dnsbl", "err", err)
|
lg.Error("can't look up ip in dnsbl", "err", err)
|
||||||
}
|
}
|
||||||
db.Set(r.Context(), ip, resp, 24*time.Hour)
|
db.Set(r.Context(), ip, resp, 24*time.Hour)
|
||||||
droneBLHits.WithLabelValues(resp.String()).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
droneBLHits.WithLabelValues(resp.String(), asn, asnDesc).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != dnsbl.AllGood {
|
if resp != dnsbl.AllGood {
|
||||||
@@ -366,7 +415,7 @@ func (s *Server) handleDNSBL(w http.ResponseWriter, r *http.Request, ip string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
localizer := localization.GetLocalizer(r)
|
localizer := localization.GetLocalizer(r)
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
@@ -435,11 +484,14 @@ func (s *Server) MakeChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
|
lg.Debug("made challenge", "challenge", chall, "rules", rule.Challenge, "cr", cr)
|
||||||
challengesIssued.WithLabelValues("api").Inc()
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesIssued.WithLabelValues("api", asn, asnDesc).Inc()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
localizer := localization.GetLocalizer(r)
|
localizer := localization.GetLocalizer(r)
|
||||||
|
|
||||||
redir := r.FormValue("redir")
|
redir := r.FormValue("redir")
|
||||||
@@ -530,7 +582,8 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := impl.Validate(r, lg, in); err != nil {
|
if err := impl.Validate(r, lg, in); err != nil {
|
||||||
failedValidations.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
failedValidations.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).Inc()
|
||||||
var cerr *challenge.Error
|
var cerr *challenge.Error
|
||||||
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
s.ClearCookie(w, CookieOpts{Path: cookiePath, Host: r.Host})
|
||||||
lg.Debug("challenge validate call failed", "err", err)
|
lg.Debug("challenge validate call failed", "err", err)
|
||||||
@@ -590,7 +643,10 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
lg.Debug("can't update information about challenge", "err", err)
|
lg.Debug("can't update information about challenge", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
challengesValidated.WithLabelValues(rule.Challenge.Algorithm).Inc()
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesValidated.WithLabelValues(rule.Challenge.Algorithm, asn, asnDesc).Inc()
|
||||||
|
}
|
||||||
lg.Debug("challenge passed, redirecting to app")
|
lg.Debug("challenge passed, redirecting to app")
|
||||||
http.Redirect(w, r, redir, http.StatusFound)
|
http.Redirect(w, r, redir, http.StatusFound)
|
||||||
}
|
}
|
||||||
@@ -629,7 +685,8 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
|
|||||||
return cr("bot/"+b.Name, b.Action, weight), &b, nil
|
return cr("bot/"+b.Name, b.Action, weight), &b, nil
|
||||||
case config.RuleWeigh:
|
case config.RuleWeigh:
|
||||||
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
|
lg.Debug("adjusting weight", "name", b.Name, "delta", b.Weight.Adjust)
|
||||||
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH").Add(1)
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
policy.Applications.WithLabelValues("bot/"+b.Name, "WEIGH", asn, asnDesc).Add(1)
|
||||||
weight += b.Weight.Adjust
|
weight += b.Weight.Adjust
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -58,7 +58,7 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf
|
|||||||
|
|
||||||
t.Logf("loading policy file: %s", fname)
|
t.Logf("loading policy file: %s", fname)
|
||||||
|
|
||||||
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info")
|
anubisPolicy, err := LoadPoliciesOrDefault(ctx, fname, difficulty, "info", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@ func TestLoadPolicies(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info"); err != nil {
|
if _, err := policy.ParseConfig(t.Context(), fin, fname, 4, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ type Options struct {
|
|||||||
DifficultyInJWT bool
|
DifficultyInJWT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) {
|
func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*policy.ParsedConfig, error) {
|
||||||
var fin io.ReadCloser
|
var fin io.ReadCloser
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty
|
|||||||
}
|
}
|
||||||
}(fin)
|
}(fin)
|
||||||
|
|
||||||
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel)
|
anubisPolicy, err := policy.ParseConfig(ctx, fin, fname, defaultDifficulty, logLevel, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
return nil, fmt.Errorf("can't parse policy file %s: %w", fname, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Logging struct {
|
|||||||
Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
|
Sink string `json:"sink"` // Logging sink, either "stdio" or "file"
|
||||||
Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags
|
Level *slog.Level `json:"level"` // Log level, if set supersedes the level in flags
|
||||||
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
|
Parameters *LoggingFileConfig `json:"parameters"` // Logging parameters, to be dynamic in the future
|
||||||
|
LogASN bool `json:"asn" yaml:"asn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestInvalidChallengeMethod(t *testing.T) {
|
func TestInvalidChallengeMethod(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info"); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
if _, err := LoadPoliciesOrDefault(t.Context(), "testdata/invalid-challenge-method.yaml", 4, "info", false); !errors.Is(err, policy.ErrChallengeRuleHasWrongAlgorithm) {
|
||||||
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
t.Fatalf("wanted error %v but got %v", policy.ErrChallengeRuleHasWrongAlgorithm, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
|
|
||||||
for _, st := range finfos {
|
for _, st := range finfos {
|
||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
@@ -44,13 +44,13 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
t.Run(st.Name(), func(t *testing.T) {
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
t.Run("with-thoth", func(t *testing.T) {
|
t.Run("with-thoth", func(t *testing.T) {
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("without-thoth", func(t *testing.T) {
|
t.Run("without-thoth", func(t *testing.T) {
|
||||||
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("config", "testdata", "good", st.Name()), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+11
-7
@@ -207,7 +207,7 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, r := s.getRequestLogger(r)
|
||||||
|
|
||||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") && randomChance(64) {
|
||||||
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
lg.Error("client was given a challenge but does not in fact support gzip compression")
|
||||||
@@ -215,7 +215,10 @@ func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, cr policy.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
challengesIssued.WithLabelValues("embedded").Add(1)
|
{
|
||||||
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
challengesIssued.WithLabelValues("embedded", asn, asnDesc).Add(1)
|
||||||
|
}
|
||||||
chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule)
|
chall, err := s.issueChallenge(r.Context(), r, lg, cr, rule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lg.Error("can't get challenge", "err", err)
|
lg.Error("can't get challenge", "err", err)
|
||||||
@@ -306,14 +309,14 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
|
|||||||
case "http", "https":
|
case "http", "https":
|
||||||
// allowed
|
// allowed
|
||||||
default:
|
default:
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
||||||
return "", errors.New(localizer.T("invalid_redirect"))
|
return "", errors.New(localizer.T("invalid_redirect"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
||||||
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Debug("domain not allowed", "domain", host)
|
lg.Debug("domain not allowed", "domain", host)
|
||||||
return "", errors.New(localizer.T("redirect_domain_not_allowed"))
|
return "", errors.New(localizer.T("redirect_domain_not_allowed"))
|
||||||
}
|
}
|
||||||
@@ -415,7 +418,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "", "http", "https":
|
case "", "http", "https":
|
||||||
// allowed: empty scheme means relative URL
|
// allowed: empty scheme means relative URL
|
||||||
default:
|
default:
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
||||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -427,7 +430,7 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
||||||
|
|
||||||
if hostNotAllowed || hostMismatch {
|
if hostNotAllowed || hostMismatch {
|
||||||
lg := internal.GetRequestLogger(s.logger, r)
|
lg, _ := s.getRequestLogger(r)
|
||||||
lg.Debug("domain not allowed", "domain", urlParsed.Host)
|
lg.Debug("domain not allowed", "domain", urlParsed.Host)
|
||||||
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
|
s.respondWithStatus(w, r, localizer.T("redirect_domain_not_allowed"), makeCode(err), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -442,7 +445,8 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
|
web.Base(localizer.T("you_are_not_a_bot"), web.StaticHappy(localizer), s.policy.Impressum, localizer),
|
||||||
).ServeHTTP(w, r)
|
).ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
requestsProxied.WithLabelValues(r.Host).Inc()
|
asn, asnDesc := asnFromContext(r.Context())
|
||||||
|
requestsProxied.WithLabelValues(r.Host, asn, asnDesc).Inc()
|
||||||
r = s.stripBasePrefixFromRequest(r)
|
r = s.stripBasePrefixFromRequest(r)
|
||||||
s.next.ServeHTTP(w, r)
|
s.next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
|||||||
ErrorLog: internal.GetFilteredHTTPLogger(),
|
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ln, metricsURL, err := internal.SetupListener(s.Config.Bind, s.Config.Network, s.Config.SocketMode)
|
ln, metricsURL, err := internal.SetupListener(s.Config.Network, s.Config.Bind, s.Config.SocketMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't setup listener: %w", err)
|
return fmt.Errorf("can't setup listener: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CELChecker struct {
|
type CELChecker struct {
|
||||||
program cel.Program
|
program cel.Program
|
||||||
src string
|
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)
|
env, err := expressions.BotEnvironment(dnsObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -29,8 +30,9 @@ func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &CELChecker{
|
return &CELChecker{
|
||||||
src: cfg.String(),
|
src: cfg.String(),
|
||||||
program: program,
|
program: program,
|
||||||
|
subRequestMode: subRequestMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ func (cc *CELChecker) Hash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -54,6 +56,7 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) {
|
|||||||
|
|
||||||
type CELRequest struct {
|
type CELRequest struct {
|
||||||
*http.Request
|
*http.Request
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
func (cr *CELRequest) Parent() cel.Activation { return nil }
|
||||||
@@ -71,6 +74,14 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) {
|
|||||||
case "userAgent":
|
case "userAgent":
|
||||||
return cr.UserAgent(), true
|
return cr.UserAgent(), true
|
||||||
case "path":
|
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
|
return cr.URL.Path, true
|
||||||
case "query":
|
case "query":
|
||||||
return expressions.URLValues{Values: cr.URL.Query()}, true
|
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")`,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("creating CEL checker failed: %v", err)
|
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")
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-8
@@ -94,23 +94,29 @@ func (hmc *HeaderMatchesChecker) Hash() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PathChecker struct {
|
type PathChecker struct {
|
||||||
regexp *regexp.Regexp
|
regexp *regexp.Regexp
|
||||||
hash string
|
hash string
|
||||||
|
subRequestMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPathChecker(rexStr string) (checker.Impl, error) {
|
func NewPathChecker(rexStr string, subrequestMode bool) (checker.Impl, error) {
|
||||||
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
rex, err := regexp.Compile(strings.TrimSpace(rexStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, rexStr, err)
|
||||||
}
|
}
|
||||||
return &PathChecker{rex, internal.FastHash(rexStr)}, nil
|
return &PathChecker{rex, internal.FastHash(rexStr), subrequestMode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
func (pc *PathChecker) Check(r *http.Request) (bool, error) {
|
||||||
originalUrl := r.Header.Get("X-Original-URI")
|
if pc.subRequestMode {
|
||||||
if originalUrl != "" {
|
originalUrl := r.Header.Get("X-Original-URI")
|
||||||
if pc.regexp.MatchString(originalUrl) {
|
if originalUrl == "" {
|
||||||
return true, nil
|
originalUrl = r.Header.Get("X-Forwarded-Uri")
|
||||||
|
}
|
||||||
|
if originalUrl != "" {
|
||||||
|
if pc.regexp.MatchString(originalUrl) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+223
-2
@@ -272,8 +272,8 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// Create the PathChecker
|
// Create the PathChecker in subrequest mode so X-Original-URI is honored.
|
||||||
pc, err := NewPathChecker(tt.regex)
|
pc, err := NewPathChecker(tt.regex, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !tt.expectError {
|
if !tt.expectError {
|
||||||
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
t.Fatalf("NewPathChecker() unexpected error: %v", err)
|
||||||
@@ -305,3 +305,224 @@ func TestPathChecker_XOriginalURI(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPathChecker_GHSA_6wcg_mqvh_fcvg is a regression test for
|
||||||
|
// https://github.com/TecharoHQ/anubis/security/advisories/GHSA-6wcg-mqvh-fcvg.
|
||||||
|
//
|
||||||
|
// PR https://github.com/TecharoHQ/anubis/pull/1015 added the ability for
|
||||||
|
// reverse proxies using Anubis in subrequest auth mode to look at the path
|
||||||
|
// of a request as there are many rules in the wild that rely on checking
|
||||||
|
// the path. This is how access to things like robots.txt or anything in the
|
||||||
|
// .well-known directory is unaffected by Anubis.
|
||||||
|
//
|
||||||
|
// However this logic was also enabled for non-subrequest deployments of Anubis,
|
||||||
|
// meaning that a specially crafted request could include a /.well-known/
|
||||||
|
// path in it and then get around Anubis with little effort.
|
||||||
|
//
|
||||||
|
// This fix gates the logic behind a new plumbed variable named subrequestMode
|
||||||
|
// that only fires when Anubis is running in subrequest auth mode. This
|
||||||
|
// properly contains that workaround so that the logic does not fire in
|
||||||
|
// most deployments.
|
||||||
|
func TestPathChecker_GHSA_6wcg_mqvh_fcvg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
regex string
|
||||||
|
urlPath string
|
||||||
|
xOriginalURI string
|
||||||
|
subRequestMode bool
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path matches",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/secret",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode ignores spoofed X-Original-URI when real path does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/public/index",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default mode without X-Original-URI matches real path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
subRequestMode: false,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode honors X-Original-URI",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/auth",
|
||||||
|
xOriginalURI: "/admin/secret",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode falls back to URL.Path when X-Original-URI does not match",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "/public/index",
|
||||||
|
subRequestMode: true,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subrequest mode with empty X-Original-URI uses URL.Path",
|
||||||
|
regex: "^/admin/.*",
|
||||||
|
urlPath: "/admin/dashboard",
|
||||||
|
xOriginalURI: "",
|
||||||
|
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.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-Original-URI=%q)",
|
||||||
|
got, tt.want, tt.subRequestMode, tt.urlPath, tt.xOriginalURI)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-4
@@ -27,7 +27,7 @@ var (
|
|||||||
Applications = promauto.NewCounterVec(prometheus.CounterOpts{
|
Applications = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
Name: "anubis_policy_results",
|
Name: "anubis_policy_results",
|
||||||
Help: "The results of each policy rule",
|
Help: "The results of each policy rule",
|
||||||
}, []string{"rule", "action"})
|
}, []string{"rule", "action", "asn", "asn_description"})
|
||||||
|
|
||||||
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
ErrChallengeRuleHasWrongAlgorithm = errors.New("config.Bot.ChallengeRules: algorithm is invalid")
|
||||||
warnedAboutThresholds = &atomic.Bool{}
|
warnedAboutThresholds = &atomic.Bool{}
|
||||||
@@ -47,6 +47,8 @@ type ParsedConfig struct {
|
|||||||
Dns *dns.Dns
|
Dns *dns.Dns
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
Metrics *config.Metrics
|
Metrics *config.Metrics
|
||||||
|
ThothClient *thoth.Client
|
||||||
|
LogASN bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
func newParsedConfig(orig *config.Config) *ParsedConfig {
|
||||||
@@ -58,7 +60,7 @@ func newParsedConfig(orig *config.Config) *ParsedConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string) (*ParsedConfig, error) {
|
func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDifficulty int, logLevel string, subrequestMode bool) (*ParsedConfig, error) {
|
||||||
c, err := config.Load(fin, fname)
|
c, err := config.Load(fin, fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -70,6 +72,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
result := newParsedConfig(c)
|
result := newParsedConfig(c)
|
||||||
result.DefaultDifficulty = defaultDifficulty
|
result.DefaultDifficulty = defaultDifficulty
|
||||||
|
result.LogASN = c.Logging.LogASN
|
||||||
|
if hasThothClient {
|
||||||
|
result.ThothClient = tc
|
||||||
|
}
|
||||||
|
|
||||||
if c.Logging.Level != nil {
|
if c.Logging.Level != nil {
|
||||||
logLevel = c.Logging.Level.String()
|
logLevel = c.Logging.Level.String()
|
||||||
@@ -94,6 +100,10 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
|
|
||||||
lg := result.Logger.With("at", "config-validate")
|
lg := result.Logger.With("at", "config-validate")
|
||||||
|
|
||||||
|
if result.LogASN && !hasThothClient {
|
||||||
|
lg.Warn("logging.asn is enabled but no Thoth client is configured; ASN logging and metrics will be skipped. Please read https://anubis.techaro.lol/docs/admin/thoth for more information")
|
||||||
|
}
|
||||||
|
|
||||||
stFac, ok := store.Get(c.Store.Backend)
|
stFac, ok := store.Get(c.Store.Backend)
|
||||||
switch ok {
|
switch ok {
|
||||||
case true:
|
case true:
|
||||||
@@ -142,7 +152,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.PathRegex != nil {
|
if b.PathRegex != nil {
|
||||||
c, err := NewPathChecker(*b.PathRegex)
|
c, err := NewPathChecker(*b.PathRegex, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s path regex: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
@@ -160,7 +170,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b.Expression != nil {
|
if b.Expression != nil {
|
||||||
c, err := NewCELChecker(b.Expression, result.Dns)
|
c, err := NewCELChecker(b.Expression, result.Dns, subrequestMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestDefaultPolicyMustParse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, "botPolicies.yaml", anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatalf("can't parse config: %v", err)
|
t.Fatalf("can't parse config: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
ctx := thothmock.WithMockThoth(t)
|
ctx := thothmock.WithMockThoth(t)
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -53,7 +53,7 @@ func TestGoodConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info"); err != nil {
|
if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -77,7 +77,7 @@ func TestBadConfigs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer fin.Close()
|
defer fin.Close()
|
||||||
|
|
||||||
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info"); err == nil {
|
if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty, "info", false); err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
|||||||
Generated
+324
-357
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -15,16 +15,18 @@
|
|||||||
"package": "go tool yeet",
|
"package": "go tool yeet",
|
||||||
"lint": "make lint",
|
"lint": "make lint",
|
||||||
"prepare": "husky && go mod download",
|
"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": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.5.0",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.5.0",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"baseline-browser-mapping": "^2.10.15",
|
"baseline-browser-mapping": "^2.10.27",
|
||||||
"cssnano": "^7.1.4",
|
"cssnano": "^7.1.8",
|
||||||
"cssnano-preset-advanced": "^7.0.12",
|
"cssnano-preset-advanced": "^7.0.16",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
"postcss-import-url": "^7.2.0",
|
"postcss-import-url": "^7.2.0",
|
||||||
"postcss-url": "^10.1.3",
|
"postcss-url": "^10.1.3",
|
||||||
"prettier": "^3.8.1"
|
"prettier": "^3.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-crypto/sha256-js": "^5.2.0",
|
"@aws-crypto/sha256-js": "^5.2.0",
|
||||||
|
|||||||
@@ -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
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
python3 -m venv .env
|
||||||
|
source .env/bin/activate
|
||||||
|
pip install pyyaml
|
||||||
|
|
||||||
python3 -c 'import yaml'
|
python3 -c 'import yaml'
|
||||||
python3 ./compare_bots.py
|
python3 ./compare_bots.py
|
||||||
|
|||||||
@@ -104,5 +104,6 @@ require (
|
|||||||
|
|
||||||
tool (
|
tool (
|
||||||
github.com/TecharoHQ/anubis/cmd/anubis
|
github.com/TecharoHQ/anubis/cmd/anubis
|
||||||
|
github.com/TecharoHQ/anubis/utils/cmd/backoff-retry
|
||||||
github.com/jsha/minica
|
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 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}
|
FROM alpine:${ALPINE_VERSION}
|
||||||
RUN apk add -U go nodejs git build-base git npm bash zstd brotli gzip
|
COPY --from=go /app/bin/go /usr/local/bin/go
|
||||||
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
|
||||||
|
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