···11+ISC License
22+33+Copyright (c) 2026 Solly "directxman12" Ross
44+55+Permission to use, copy, modify, and/or distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1010+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1111+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1212+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1313+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1515+PERFORMANCE OF THIS SOFTWARE.
+202
README.md
···11+# An OIDC Reverse Proxy, Written in Anger
22+33+## What is it?
44+55+It's a multi-tentant non-caching tls-terminating [reverse proxy] that can
66+terminate a hybrid of [OpenID Connect Core v1.0 with errata 2][oidc-v1] [^1]
77+and [OAuth 2.1][oauth-v2.1] [^2]. It also generally reverse proxies HTTP 1.x,
88+h2, websocket, and grpc traffic, as it's built (right now) on [pingora].
99+1010+## ... what?
1111+1212+You stick in front of webapps that need either tls added or don't support
1313+checking oauth2 state themselves, and it does that for you.
1414+1515+Think [oauth2-proxy], but with proper multitenancy support and significantly
1616+less battle-tested.
1717+1818+## whyyyy????
1919+2020+I [was](https://bsky.app/profile/directxman12.dev/post/3me6doan42k2t) [frustrated](https://bsky.app/profile/directxman12.dev/post/3merwp3aik22u) with the state of oauth2/oidc-terminating proxies.
2121+2222+Particularly, at the time of writing, [oauth2-proxy] does not support
2323+multitenancy properly (it encourages the insecure practice of sharing
2424+client_ids if you use it for forward auth).
2525+2626+Other things, like [caddy-security], felt overcomplicated for what i needed to
2727+do, and tbh i don't trust security tools that feel like they have everything
2828+and the the kitchen sink shoved in, unless they've been written by experts
2929+and/or extensively auditted.
3030+3131+[caddy-security]: https://github.com/greenpau/caddy-security
3232+3333+## how do i use it?
3434+3535+The configuration is done in [textproto] format [^3].
3636+The format is in <src/config/format.proto>, and the inline docs serve as the
3737+documentation, so i'd suggest starting there.
3838+3939+Here's a quick example config:
4040+4141+```textproto
4242+# bind on ipv4
4343+bind_to_tcp {
4444+ addr: '0.0.0.0:8443'
4545+ tls {
4646+ cert_path: "/var/lib/secrets/serving.cert"
4747+ key_path: "/var/lib/secrets/serving.key"
4848+ }
4949+}
5050+# bind on ipv6
5151+bind_to_tcp {
5252+ addr: '[::]:8443'
5353+ tls {
5454+ cert_path: "/var/lib/secrets/serving.cert"
5555+ key_path: "/var/lib/secrets/serving.key"
5656+ }
5757+}
5858+5959+# serve these domains
6060+6161+# serve copyparty w/ oidc termination on
6262+domains {
6363+ key: "files.example.com"
6464+ value: {
6565+ # serve to a (single, here, but this is a `repeated` field)
6666+ # uds backend, without tls
6767+ uds {
6868+ path: "/run/copyparty/party.sock"
6969+ }
7070+ # enable oidc termination on this domain
7171+ oidc_auth {
7272+ # standard oidc/oauth stuff
7373+ discovery_url_base: "https://sso.example.com/oauth2/openid/files/"
7474+ client_id: "files"
7575+7676+ # this is where we redirect after logout
7777+ logout_url: "https://sso.example.com/"
7878+ # put your client secret here
7979+ client_secret_path: '/var/lib/secrets/files.client-secret'
8080+8181+ # these scopes are required, and sent in the request
8282+ scopes {
8383+ required: "profile"
8484+ required: "group_names"
8585+ required: "party"
8686+ }
8787+ # these claims are mapped to headers
8888+ claims {
8989+ claim_to_header {
9090+ key: "group_names"
9191+ value: "X-Idp-Groups"
9292+ }
9393+ claim_to_header {
9494+ key: "name"
9595+ value: "X-Idp-User"
9696+ }
9797+ },
9898+ }
9999+ }
100100+}
101101+102102+# serve your sso server, with oidc termination off
103103+domains {
104104+ key: "sso.example.com"
105105+ value: {
106106+ # your sso server might have it's own tls termination too, so
107107+ # connect to it with tls
108108+ https {
109109+ addr: "127.0.0.1:1309"
110110+ }
111111+ }
112112+}
113113+```
114114+115115+[textproto]: https://protobuf.dev/reference/protobuf/textformat-spec/
116116+117117+[^3]: **Q**: wait, seriously? **A**: yes, seriously. i'm putting my money
118118+ where my mouth is, so to speak, when i say that textproto is nice, actually.
119119+120120+## should i trust this?
121121+122122+**_no, probably not._**
123123+124124+consider the following points if you chose to run this:
125125+126126+- i make no claims about it's security.
127127+128128+- i'm not a security professional, just a dev with a hobby of writing oauth
129129+ implementations in anger, apparently [^4].
130130+131131+- if you're often targetted by script kiddies or nation-state actors,
132132+ or you're working for a company or organization, please don't use this.
133133+134134+- it'll probably be fine for low-stress homelab situations? that's what i'm using
135135+ it for.
136136+137137+[^4]: last year i wrote (but haven't published yet) a cute little oauth server
138138+ that uses exclusively passkeys from scratch.
139139+140140+## but does it support...
141141+142142+<dl>
143143+<dt>...serving forward auth?</dt>
144144+<dd>
145145+146146+no, not right now. forward auth is an [underspecified mess][forward-auth-mess],
147147+so it's unlikely that i'll add support.
148148+149149+</dd>
150150+151151+<dt>...authorization?</dt>
152152+<dd>
153153+154154+nope, this was written for reverse-proxying apps that either
155155+156156+a) can make their own auth decisions (like [copyparty]), or
157157+b) don't need to make complex auth decisions
158158+159159+</dd>
160160+161161+<dt>...other types of authn?</dt>
162162+<dd>
163163+164164+nope, and it's unlikely that it will -- i prefer to keep things well-scoped.
165165+unless it's something odic/oauth2-adjacent, in which case i'd consider it.
166166+167167+</dd>
168168+169169+<dt>...different certs per domain</dt>
170170+<dd>not yet, but i'd like to get to it eventually</dd>
171171+172172+</dl>
173173+174174+For anything else, check the [issue tracker] and/or file an issue.
175175+176176+## omg the code is messsyyyyyy!
177177+178178+yeah, it probably needs further refactoring.
179179+180180+## Contributing
181181+182182+On the off-chance that you'd like to submit code, i'll take a look at it and
183183+review it, but i'm likely going to be picky about what new features i accept.
184184+it's probably worth filing an issue to discuss first.
185185+186186+any code submitted should be your own, and you should understand it fully.
187187+188188+[forward-auth-mess]: https://github.com/kanidm/kanidm/issues/2774
189189+[issue tracker]: https://tangled.org/directxman12.dev/proxy-in-anger/issues
190190+[copyparty]: https://github.com/9001/copyparty
191191+192192+193193+194194+[reverse proxy]: https://en.wikipedia.org/wiki/Reverse_proxy
195195+[oidc-v1]: https://openid.net/specs/openid-connect-core-1_0.html
196196+[oauth-v2.1]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1
197197+[pingora]: https://github.com/cloudflare/pingora
198198+[oauth2-proxy]: https://github.com/oauth2-proxy/oauth2-proxy
199199+200200+201201+[^1]: the uuuh... reasonable parts? yeesh, there's a lotta junk in oidc core v1
202202+[^2]: specifically, the authorization code grant flow
+60-3
src/config/format.proto
···33package config.format;
4455// the root config
66+//
77+// each of these fields will show up at the root of your config file
88+//
99+// at the minimum, you'll need at least 1 `domains` entry to define your
1010+// sites, and one `bind_to_tcp` entry to define how the proxy itself
1111+// serves content.
612message Config {
713 // the domains to serve
814 map<string, Domain> domains = 1;
···1117 repeated TCPBinding bind_to_tcp = 2;
12181319 // lower-level pingora config
2020+ //
2121+ // (you probably don't need to touch this)
1422 Pingora pingora = 3;
1523}
16242525+// a single, served domain
2626+//
2727+// you'll want at least 1 backend (`https`, `http`, or `uds`),
2828+// but you can repeat each one to define multiple backends
2929+// and load-balance between them, and you can (if you really need
3030+// to) mix and match types.
3131+//
3232+// you may also want an (optional) `oidc_auth` configuration to turn on
3333+// authentication-termination
3434+//
3535+// you can also use `manage_headers` to inject things like `X-Forwarded-For`,
3636+// or clear headers that you don't want going to your backend.
1737message Domain {
1838 // require oidc auth if this is set
1939 optional OIDC oidc_auth = 1;
···3454 TLS_MODE_UNSAFE_ALLOW_HTTP = 1;
3555 }
36565757+ // allow disabling TLS termination, for testing
3758 TLSMode tls_mode = 5;
38593960 // set or clear headers on the backend request
4061 ManageHeaders manage_headers = 6;
4162}
42636464+// configure oidc/oauth v2.1 termination
4365message OIDC {
4466 // the base oidc discovery url, without the `.well-known/openid-configuration` part
4567 //
4668 // per [OIDC Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)
6969+ // and [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414).
4770 string discovery_url_base = 1;
48717272+ // your oauth client id, from your oauth provider
4973 string client_id = 2;
7474+ // your oauth client secret, from your oauth provider
5075 string client_secret_path = 3;
51767777+ // a set of scopes you wish to ask the sever for
7878+ //
7979+ // (`openid` will always be automatically included)
5280 Scopes scopes = 4;
8181+ // map returned "claims" (pieces of user information)
8282+ // to header in the backend request.
8383+ //
8484+ // for example, you can use this to tell backend services the name of the
8585+ // authenticated user.
5386 Claims claims = 5;
54875588 // per oidc core v1-with-errata-2§3.1.3.7 point 6, we _may_ skip validation
···5790 // case. some folks may want to be extra paranoid, but generally you either
5891 // trust tls, or you can't trust discovery, and thus can't trust the jwks info,
5992 // so default this to false.
9393+ //
9494+ // generally, you can leave this off.
6095 bool validate_with_jwk = 6;
61966297 // where to redirect to on logout
6398 string logout_url = 7;
6499}
65100101101+// scopes to ask the server for
66102message Scopes {
6767- repeated string optional = 1;
103103+ // the scopes you need from the server for your claims map to work, or that
104104+ // you want to request to ensure that the user is authorized to continue to
105105+ // your site.
68106 repeated string required = 2;
107107+108108+ // optional scopes, not requested from the server
109109+ //
110110+ // not currently used
111111+ repeated string optional = 1;
69112}
7011371114// information on how to process returned claims
···110153}
111154112155// equivalent to [`pingora::server::configuration::Config`]
156156+//
157157+// See [pingora](https://docs.rs/pingora/latest/pingora/server/configuration/struct.ServerConf.html)
158158+// for details on what these do
113159message Pingora {
114160 uint64 version = 1;
115161 bool daemon = 2;
···135181136182}
137183184184+// bind to a tcp port
185185+//
186186+// this configures which ports you'll serve https & grpc traffic on,
187187+// and how you'll terminate tls.
188188+//
189189+// you'll want to chose an address, as well as point at your tls certs.
138190message TCPBinding {
139139- // configure used tls settings
191191+ // configure tls settings
140192 message TLS {
141193 // path to the (public) tls certificate, with all intermediate certificates (fullchain.pem for most acme clients)
142194 string cert_path = 1;
···146198147199 // host an port to bind to
148200 string addr = 1;
149149- // tls, if desired
201201+202202+ // enable tls
203203+ //
204204+ // you should _always_ have this, unless you're testing
205205+ //
206206+ // oidc will not work without it
150207 optional TLS tls = 2;
151208152209 // TODO(feature): surface tcp options from pingora