Add check endpoint which can be used with nginx' auth_request function (#266)

* Add check endpoint which can be used with nginx' auth_request function

* feat(cmd): allow configuring redirect domains

* test: add test environment for the nginx_auth PR

This is a full local setup of the nginx_auth PR including HTTPS so that
it's easier to validate in isolation.

This requires an install of k3s (https://k3s.io) with traefik set to
listen on localhost. This will be amended in the future but for now this
works enough to ship it.

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(cmd|lib): allow empty redirect domains variable

Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(test): add space to target variable in anubis container

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(admin): rewrite subrequest auth docs, make generic

* docs(install): document REDIRECT_DOMAINS flag

Signed-off-by: Xe Iaso <me@xeiaso.net>

* feat(lib): clamp redirects to the same HTTP host

Only if REDIRECT_DOMAINS is not set.

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Sandro 2025-04-25 19:38:02 +02:00 committed by GitHub
parent a5d796c679
commit 6858f66a62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 610 additions and 52 deletions

View File

@ -50,8 +50,9 @@ var (
socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.")
robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots")
policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)")
redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.")
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to")
target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request")
healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis")
useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal")
debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate")
@ -195,10 +196,15 @@ func main() {
return
}
rp, err := makeReverseProxy(*target)
var rp http.Handler
// when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space
if strings.TrimSpace(*target) != "" {
var err error
rp, err = makeReverseProxy(*target)
if err != nil {
log.Fatalf("can't make reverse proxy: %v", err)
}
}
policy, err := libanubis.LoadPoliciesOrDefault(*policyFname, *challengeDifficulty)
if err != nil {
@ -252,6 +258,20 @@ func main() {
slog.Warn("generating random key, Anubis will have strange behavior when multiple instances are behind the same load balancer target, for more information: see https://anubis.techaro.lol/docs/admin/installation#key-generation")
}
var redirectDomainsList []string
if *redirectDomains != "" {
domains := strings.Split(*redirectDomains, ",")
for _, domain := range domains {
_, err = url.Parse(domain)
if err != nil {
log.Fatalf("cannot parse redirect-domain %q: %s", domain, err.Error())
}
redirectDomainsList = append(redirectDomainsList, strings.TrimSpace(domain))
}
} else {
slog.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains")
}
s, err := libanubis.New(libanubis.Options{
Next: rp,
Policy: policy,
@ -261,6 +281,7 @@ func main() {
CookiePartitioned: *cookiePartitioned,
OGPassthrough: *ogPassthrough,
OGTimeToLive: *ogTimeToLive,
RedirectDomains: redirectDomainsList,
Target: *target,
WebmasterEmail: *webmasterEmail,
})

View File

@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Refactor check logic to be more generic and work on a Checker type
- Add more AI user agents based on the [ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) project
- Embedded challenge data in initial HTML response to improve performance
- Added support to use Nginx' `auth_request` directive with Anubis
- Added support to allow to restrict the allowed redirect domains
- Whitelisted [DuckDuckBot](https://duckduckgo.com/duckduckgo-help-pages/results/duckduckbot/) in botPolicies
- Improvements to build scripts to make them less independent of the build host
- Improved the OpenGraph error logging

View File

@ -0,0 +1,94 @@
---
title: Redirect Domain Configuration
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Anubis has an HTTP redirect in the middle of its check validation logic. This redirect allows Anubis to set a cookie on validated requests so that users don't need to pass challenges on every page load.
This flow looks something like this:
```mermaid
sequenceDiagram
participant User
participant Challenge
participant Validation
participant Backend
User->>+Challenge: GET /
Challenge->>+User: Solve this challenge
User->>+Validation: Here's the solution, send me to /
Validation->>+User: Here's a cookie, go to /
User->>+Backend: GET /
```
However, in some cases a sufficiently dedicated attacker could trick a user into clicking on a validation link with a solution pre-filled out. For example:
```mermaid
sequenceDiagram
participant Hacker
participant User
participant Validation
participant Evil Site
Hacker->>+User: Click on yoursite.com with this solution
User->>+Validation: Here's a solution, send me to evilsite.com
Validation->>+User: Here's a cookie, go to evilsite.com
User->>+Evil Site: GET evilsite.com
```
If this happens, Anubis will throw an error like this:
```text
Redirect domain not allowed
```
## Configuring allowed redirect domains
By default, Anubis will limit redirects to be on the same HTTP Host that Anubis is running on (EG: requests to yoursite.com cannot redirect outside of yoursite.com). If you need to set more than one domain, fill the `REDIRECT_DOMAINS` environment variable with a comma-separated list of domain names that Anubis should allow redirects to.
:::note
These domains are _an exact string match_, they do not support wildcard matches.
:::
<Tabs>
<TabItem value="env-file" label="Environment file" default>
```shell
# anubis.env
REDIRECT_DOMAINS="yoursite.com,secretplans.yoursite.com"
# ...
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
```yaml
services:
anubis-nginx:
image: ghcr.io/techarohq/anubis:latest
environment:
REDIRECT_DOMAINS: "yoursite.com,secretplans.yoursite.com"
# ...
```
</TabItem>
<TabItem value="k8s" label="Kubernetes">
Inside your Deployment, StatefulSet, or Pod:
```yaml
- name: anubis
image: ghcr.io/techarohq/anubis:latest
env:
- name: REDIRECT_DOMAINS
value: "yoursite.com,secretplans.yoursite.com"
# ...
```
</TabItem>
</Tabs>

View File

@ -0,0 +1,139 @@
---
title: Subrequest Authentication
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
Anubis can act in one of two modes:
1. Reverse proxy (the default): Anubis sits in the middle of all traffic and then will reverse proxy it to its destination. This is the moral equivalent of a middleware in your favorite web framework.
2. Subrequest authentication mode: Anubis listens for requests and if they don't pass muster then they are forwarded to Anubis for challenge processing. This is the equivalent of Anubis being a sidecar service.
## Nginx
Anubis can perform [subrequest authentication](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/) with the `auth_request` module in Nginx. In order to set this up, keep the following things in mind:
The `TARGET` environment variable in Anubis must be set to a space, eg:
<Tabs>
<TabItem value="env-file" label="Environment file" default>
```shell
# anubis.env
TARGET=" "
# ...
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
```yaml
services:
anubis-nginx:
image: ghcr.io/techarohq/anubis:latest
environment:
TARGET: " "
# ...
```
</TabItem>
<TabItem value="k8s" label="Kubernetes">
Inside your Deployment, StatefulSet, or Pod:
```yaml
- name: anubis
image: ghcr.io/techarohq/anubis:latest
env:
- name: TARGET
value: " "
# ...
```
</TabItem>
</Tabs>
In order to configure this, you need to add the following location blocks to each server pointing to the service you want to protect:
```nginx
location /.within.website/ {
# Assumption: Anubis is running in the same network namespace as
# nginx on localhost TCP port 8923
proxy_pass http://127.0.0.1:8923;
auth_request off;
}
location @redirectToAnubis {
return 307 /.within.website/?redir=$scheme://$host$request_uri;
auth_request off;
}
```
This sets up `/.within.website` to point to Anubis. Any requests that Anubis rejects or throws a challenge to will be sent here. This also sets up a named location `@redirectToAnubis` that will redirect any requests to Anubis for advanced processing.
Finally, add this to your root location block:
```nginx
location / {
# diff-add
auth_request /.within.website/x/cmd/anubis/api/check;
# diff-add
error_page 401 = @redirectToAnubis;
}
```
This will check all requests that don't match other locations with Anubis to ensure the client is genuine.
This will make every request get checked by Anubis before it hits your backend. If you have other locations that don't need Anubis to do validation, add the `auth_request off` directive to their blocks:
```nginx
location /secret {
# diff-add
auth_request off;
# ...
}
```
Here is a complete example of an Nginx server listening over TLS and pointing to Anubis:
<details>
<summary>Complete example</summary>
```nginx
# /etc/nginx/conf.d/nginx.local.cetacean.club.conf
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name nginx.local.cetacean.club;
ssl_certificate /etc/techaro/pki/nginx.local.cetacean.club/tls.crt;
ssl_certificate_key /etc/techaro/pki/nginx.local.cetacean.club/tls.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /.within.website/ {
proxy_pass http://localhost:8923;
auth_request off;
}
location @redirectToAnubis {
return 307 /.within.website/?redir=$scheme://$host$request_uri;
auth_request off;
}
location / {
auth_request /.within.website/x/cmd/anubis/api/check;
error_page 401 = @redirectToAnubis;
root /usr/share/nginx/html;
index index.html index.htm;
}
}
```
</details>

View File

@ -50,7 +50,7 @@ For more detailed information on installing Anubis with native packages, please
Anubis uses these environment variables for configuration:
| Environment Variable | Default value | Explanation |
| :----------------------------- | :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| :----------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BIND` | `:8923` | The network address that Anubis listens on. For `unix`, set this to a path: `/run/anubis/instance.sock` |
| `BIND_NETWORK` | `tcp` | The address family that Anubis listens on. Accepts `tcp`, `unix` and anything Go's [`net.Listen`](https://pkg.go.dev/net#Listen) supports. |
| `COOKIE_DOMAIN` | unset | The domain the Anubis challenge pass cookie should be set to. This should be set to the domain you bought from your registrar (EG: `techaro.lol` if your webapp is running on `anubis.techaro.lol`). See [here](https://stackoverflow.com/a/1063760) for more information. |
@ -63,6 +63,7 @@ Anubis uses these environment variables for configuration:
| `OG_EXPIRY_TIME` | `24h` | The expiration time for the Open Graph tag cache. |
| `OG_PASSTHROUGH` | `false` | If set to `true`, Anubis will enable Open Graph tag passthrough. |
| `POLICY_FNAME` | unset | The file containing [bot policy configuration](./policies.mdx). See the bot policy documentation for more details. If unset, the default bot policy configuration is used. |
| `REDIRECT_DOMAINS` | unset | If set, restrict the domains that Anubis can redirect to when passing a challenge.<br/><br/>If this is unset, Anubis may redirect to any domain which could cause security issues in the unlikely case that an attacker passes a challenge for your browser and then tricks you into clicking a link to your domain. |
| `SERVE_ROBOTS_TXT` | `false` | If set `true`, Anubis will serve a default `robots.txt` file that disallows all known AI scrapers by name and then additionally disallows every scraper. This is useful if facts and circumstances make it difficult to change the underlying service to serve such a `robots.txt` file. |
| `SOCKET_MODE` | `0770` | _Only used when at least one of the `*_BIND_NETWORK` variables are set to `unix`._ The socket mode (permissions) for Unix domain sockets. |
| `TARGET` | `http://localhost:3923` | The URL of the service that Anubis should forward valid requests to. Supports Unix domain sockets, set this to a URI like so: `unix:///path/to/socket.sock`. |

View File

@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"os"
"slices"
"strconv"
"strings"
"time"
@ -66,6 +67,7 @@ var (
type Options struct {
Next http.Handler
Policy *policy.ParsedConfig
RedirectDomains []string
ServeRobotsTXT bool
PrivateKey ed25519.PrivateKey
@ -148,9 +150,10 @@ func New(opts Options) (*Server, error) {
mux.HandleFunc("POST /.within.website/x/cmd/anubis/api/make-challenge", result.MakeChallenge)
mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/pass-challenge", result.PassChallenge)
mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/check", result.maybeReverseProxyHttpStatusOnly)
mux.HandleFunc("GET /.within.website/x/cmd/anubis/api/test-error", result.TestError)
mux.HandleFunc("/", result.MaybeReverseProxy)
mux.HandleFunc("/", result.maybeReverseProxyOrPage)
result.mux = mux
@ -172,6 +175,36 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
if s.next == nil {
redir := r.FormValue("redir")
urlParsed, err := r.URL.Parse(redir)
if err != nil {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
}
if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
} else if urlParsed.Host != r.URL.Host {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
}
if redir != "" {
http.Redirect(w, r, redir, http.StatusFound)
return
}
templ.Handler(
web.Base("You are not a bot!", web.StaticHappy()),
).ServeHTTP(w, r)
} else {
s.next.ServeHTTP(w, r)
}
}
func (s *Server) challengeFor(r *http.Request, difficulty int) string {
fp := sha256.Sum256(s.priv.Seed())
@ -187,7 +220,15 @@ func (s *Server) challengeFor(r *http.Request, difficulty int) string {
return internal.SHA256sum(challengeData)
}
func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
func (s *Server) maybeReverseProxyHttpStatusOnly(w http.ResponseWriter, r *http.Request) {
s.maybeReverseProxy(w, r, true)
}
func (s *Server) maybeReverseProxyOrPage(w http.ResponseWriter, r *http.Request) {
s.maybeReverseProxy(w, r, false)
}
func (s *Server) maybeReverseProxy(w http.ResponseWriter, r *http.Request, httpStatusOnly bool) {
lg := slog.With(
"user_agent", r.UserAgent(),
"accept_language", r.Header.Get("Accept-Language"),
@ -233,7 +274,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
switch cr.Rule {
case config.RuleAllow:
lg.Debug("allowing traffic to origin (explicit)")
s.next.ServeHTTP(w, r)
s.ServeHTTPNext(w, r)
return
case config.RuleDeny:
s.ClearCookie(w)
@ -264,21 +305,21 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if err != nil {
lg.Debug("cookie not found", "path", r.URL.Path)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if err := ckie.Valid(); err != nil {
lg.Debug("cookie is invalid", "err", err)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if time.Now().After(ckie.Expires) && !ckie.Expires.IsZero() {
lg.Debug("cookie expired", "path", r.URL.Path)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -289,14 +330,14 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if err != nil || !token.Valid {
lg.Debug("invalid token", "path", r.URL.Path, "err", err)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
if randomJitter() {
r.Header.Add("X-Anubis-Status", "PASS-BRIEF")
lg.Debug("cookie is not enrolled into secondary screening")
s.next.ServeHTTP(w, r)
s.ServeHTTPNext(w, r)
return
}
@ -304,7 +345,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if !ok {
lg.Debug("invalid token claims type", "path", r.URL.Path)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
challenge := s.challengeFor(r, rule.Challenge.Difficulty)
@ -312,7 +353,7 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
if claims["challenge"] != challenge {
lg.Debug("invalid challenge", "path", r.URL.Path)
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
@ -329,16 +370,22 @@ func (s *Server) MaybeReverseProxy(w http.ResponseWriter, r *http.Request) {
lg.Debug("invalid response", "path", r.URL.Path)
failedValidations.Inc()
s.ClearCookie(w)
s.RenderIndex(w, r, rule)
s.RenderIndex(w, r, rule, httpStatusOnly)
return
}
slog.Debug("all checks passed")
r.Header.Add("X-Anubis-Status", "PASS-FULL")
s.next.ServeHTTP(w, r)
s.ServeHTTPNext(w, r)
}
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot, returnHTTPStatusOnly bool) {
if returnHTTPStatusOnly {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Authorization required"))
return
}
func (s *Server) RenderIndex(w http.ResponseWriter, r *http.Request, rule *policy.Bot) {
lg := slog.With(
"user_agent", r.UserAgent(),
"accept_language", r.Header.Get("Accept-Language"),
@ -470,6 +517,19 @@ func (s *Server) PassChallenge(w http.ResponseWriter, r *http.Request) {
timeTaken.Observe(elapsedTime)
response := r.FormValue("response")
urlParsed, err := r.URL.Parse(redir)
if err != nil {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect URL not parseable", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
}
if len(urlParsed.Host) > 0 && len(s.opts.RedirectDomains) != 0 && !slices.Contains(s.opts.RedirectDomains, urlParsed.Host) {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
} else if urlParsed.Host != r.URL.Host {
templ.Handler(web.Base("Oh noes!", web.ErrorPage("Redirect domain not allowed", s.opts.WebmasterEmail)), templ.WithStatus(http.StatusInternalServerError)).ServeHTTP(w, r)
return
}
challenge := s.challengeFor(r, rule.Challenge.Difficulty)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/TecharoHQ/anubis"
@ -184,6 +185,7 @@ func TestCookieSettings(t *testing.T) {
}
if resp.StatusCode != http.StatusFound {
resp.Write(os.Stderr)
t.Errorf("wanted %d, got: %d", http.StatusFound, resp.StatusCode)
}

View File

@ -0,0 +1,6 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}

View File

@ -0,0 +1,13 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: cert-manager
namespace: kube-system
spec:
repo: https://charts.jetstack.io
chart: cert-manager
targetNamespace: cert-manager
createNamespace: true
set:
installCRDs: "true"
"prometheus.enabled": "false"

View File

@ -0,0 +1,25 @@
server {
listen 80;
listen [::]:80;
server_name nginx.local.cetacean.club;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /.within.website/ {
proxy_pass http://localhost:8923;
auth_request off;
}
location @redirectToAnubis {
return 307 /.within.website/?redir=$scheme://$host$request_uri;
auth_request off;
}
location / {
auth_request /.within.website/x/cmd/anubis/api/check;
error_page 401 = @redirectToAnubis;
root /usr/share/nginx/html;
index index.html index.htm;
}
}

View File

@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-external-auth
spec:
selector:
matchLabels:
app: nginx-external-auth
template:
metadata:
labels:
app: nginx-external-auth
spec:
volumes:
- name: config
configMap:
name: nginx-cfg
containers:
- name: www
image: nginx:alpine
resources:
limits:
memory: "128Mi"
cpu: "500m"
requests:
memory: "128Mi"
cpu: "500m"
ports:
- containerPort: 80
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: anubis
image: ttl.sh/techaro/anubis-external-auth:latest
imagePullPolicy: Always
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 250m
memory: 128Mi
env:
- name: TARGET
value: " "
- name: REDIRECT_DOMAINS
value: nginx.local.cetacean.club

View File

@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-external-auth
labels:
name: nginx-external-auth
annotations:
cert-manager.io/cluster-issuer: "selfsigned"
spec:
ingressClassName: traefik
tls:
- hosts:
- nginx.local.cetacean.club
secretName: nginx-local-cetacean-club-public-tls
rules:
- host: nginx.local.cetacean.club
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: nginx-external-auth
port:
name: http

View File

@ -0,0 +1,10 @@
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
configMapGenerator:
- name: nginx-cfg
behavior: create
files:
- ./conf.d/default.conf

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: nginx-external-auth
spec:
selector:
app: nginx-external-auth
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
type: ClusterIP

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# Build container image
(
cd ../.. \
&& npm ci \
&& npm run container -- \
--docker-repo ttl.sh/techaro/anubis-external-auth \
--docker-tags ttl.sh/techaro/anubis-external-auth:latest
)
kubectl apply -k .
echo "open https://nginx.local.cetacean.club, press control c when done"
control_c() {
kubectl delete -k .
exit
}
trap control_c SIGINT
sleep infinity

2
test/pki/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Anubis works!</title>
<link rel="stylesheet" href="/.within.website/x/xess/xess.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body id="top">
<main>
<h1>Anubis works!</h1>
<p>If you see this, everything has gone according to keikaku.</p>
<img height=128 src="/.within.website/x/cmd/anubis/static/img/happy.webp"/>
</main>
</body>
</html>

View File

@ -126,6 +126,18 @@ templ errorPage(message string, mail string) {
</div>
}
templ StaticHappy() {
<div class="centered-div">
<img
style="display:none;"
style="width:100%;max-width:256px;"
src={ "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
anubis.Version }
/>
<p>This is just a check endpoint for your reverse proxy to use.</p>
</div>
}
templ bench() {
<div style="height:20rem;display:flex">
<table style="margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem">

65
web/index_templ.go generated
View File

@ -297,7 +297,7 @@ func errorPage(message string, mail string) templ.Component {
})
}
func bench() templ.Component {
func StaticHappy() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@ -318,34 +318,77 @@ func bench() templ.Component {
templ_7745c5c3_Var16 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"centered-div\"><img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" +
anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 154, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 135, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\"><p>This is just a check endpoint for your reverse proxy to use.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 157, Col: 118}
return nil
})
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
func bench() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil {
templ_7745c5c3_Var18 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div style=\"height:20rem;display:flex\"><table style=\"margin-top:1rem;display:grid;grid-template:auto 1fr/auto auto;gap:0 0.5rem\"><thead style=\"border-bottom:1px solid black;padding:0.25rem 0;display:grid;grid-template:1fr/subgrid;grid-column:1/-1\"><tr id=\"table-header\" style=\"display:contents\"><th style=\"width:4.5rem\">Time</th><th style=\"width:4rem\">Iters</th></tr><tr id=\"table-header-compare\" style=\"display:none\"><th style=\"width:4.5rem\">Time A</th><th style=\"width:4rem\">Iters A</th><th style=\"width:4.5rem\">Time B</th><th style=\"width:4rem\">Iters B</th></tr></thead> <tbody id=\"results\" style=\"padding-top:0.25rem;display:grid;grid-template-columns:subgrid;grid-auto-rows:min-content;grid-column:1/-1;row-gap:0.25rem;overflow-y:auto;font-variant-numeric:tabular-nums\"></tbody></table><div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" +
anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 166, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"><p id=\"status\" style=\"max-width:256px\">Loading...</p><script async type=\"module\" src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs("/.within.website/x/cmd/anubis/static/js/bench.mjs?cacheBuster=" + anubis.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 169, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"></script><div id=\"sparkline\"></div><noscript><p>Running the benchmark tool requires JavaScript to be enabled.</p></noscript></div></div><form id=\"controls\" style=\"position:fixed;top:0.5rem;right:0.5rem\"><div style=\"display:flex;justify-content:end\"><label for=\"difficulty-input\" style=\"margin-right:0.5rem\">Difficulty:</label> <input id=\"difficulty-input\" type=\"number\" name=\"difficulty\" style=\"width:3rem\"></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"algorithm-select\" style=\"margin-right:0.5rem\">Algorithm:</label> <select id=\"algorithm-select\" name=\"algorithm\"></select></div><div style=\"margin-top:0.25rem;display:flex;justify-content:end\"><label for=\"compare-select\" style=\"margin-right:0.5rem\">Compare:</label> <select id=\"compare-select\" name=\"compare\"><option value=\"NONE\">-</option></select></div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}