From c13e0e12ead407166924c191a28263ebf33447dd Mon Sep 17 00:00:00 2001 From: RPJosh Date: Fri, 23 Jun 2023 21:19:56 +0200 Subject: [PATCH] Implement gRPC endpoint for envoy --- .dockerignore | 1 + MyNotes.md | 86 ++++ config.template.yml | 13 + envoy-proto/ext-auth.proto | 144 +++++++ go.mod | 18 +- go.sum | 30 ++ internal/commands/services.go | 75 +++- internal/configuration/schema/keys.go | 3 + internal/configuration/schema/server.go | 17 +- internal/handlers/handler_authz_grpc.go | 399 ++++++++++++++++++ internal/server/handlers.go | 11 +- internal/server/server.go | 98 ++++- internal/suites/Standalone/configuration.yml | 19 +- .../compose/authelia/Dockerfile.backend | 1 + .../authelia/resources/entrypoint-frontend.sh | 2 +- 15 files changed, 882 insertions(+), 35 deletions(-) create mode 100644 MyNotes.md create mode 100644 envoy-proto/ext-auth.proto create mode 100644 internal/handlers/handler_authz_grpc.go diff --git a/.dockerignore b/.dockerignore index a3946b784..4e3e8dd06 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ !entrypoint.sh !healthcheck.sh !.healthcheck.env +!dist/public_html/ \ No newline at end of file diff --git a/MyNotes.md b/MyNotes.md new file mode 100644 index 000000000..f02b9ec6c --- /dev/null +++ b/MyNotes.md @@ -0,0 +1,86 @@ +# Ausführen + +Um die Anwendung lokal auszuführen, können die folgenden Befehle verwendet werden. + +``` +export GOPATH=/tmp +source bootstrap.sh +authelia-scripts suites setup Standalone +``` + +Nun sollte der "Haupt-Enpunkt" unter `https://home.example.com:8080` und die API unter `https://authelia.example.com:9091` erreichbar sein. Achtung: es wird ein selbstsigniertes Zertifikat verwendet! +Mithilfe der Hot-Reload kann jetzt gecoded werden. + +--- + +Nach der Entwicklung kann die Testumgebung durch den folgenden Befehl wieder zurückgesetzt werden. + +``` +go run ./cmd/authelia-scripts/ suites teardown Standalone +``` + +## Benutzerdefinierte Zertifikate + +Um ein benutzerdefiniertes Zertifikat für die Ausführung zu verwenden, muss die Datai `public.backend.crt` und `private.bakend.pem` unter [diesem](/internal/suites/common/pki/) Verzeichnis abgeändert werden. +Um die Gültigkeit zu testen, kann der folgendende Befehl ausgeführt werden. + +``` +curl https://auth.rpjosh.de:9091 --connect-to 'auth.rpjosh.de:9091:authelia.example.com:9091' +``` + +## Externe erreichbarkeit + +Im aktuellen Zustand sind die Endpunkte nur unter den Docker internen IP-Adressen erreichbar. Daher muss noch ein NAT Regel angelegt werden. + +``` +ip=$(ping -c 1 authelia.example.com | gawk -F'[()]' '/PING/{print $2}') +sudo iptables -t nat -A PREROUTING -p tcp --dport 9091 -d 192.168.0.15 -j DNAT --to-destination 192.168.240.50:9091 -m comment --comment "Authelia-Test" +sudo iptables -t nat -A PREROUTING -p tcp --dport 9092 -d 192.168.0.15 -j DNAT --to-destination 192.168.240.50:9092 -m comment --comment "Authelia-Test" +sudo iptables -t nat -I OUTPUT -p tcp -o lo --dport 9091 -j DNAT --to-destination 192.168.240.50:9091 +``` + +# Customizations + +Für das Starten des *gRPC* Servers müssen die folgenden Abhängigkeiten installiert werden. + +``` +go get github.com/envoyproxy/go-control-plane +go get github.com/envoyproxy/go-control-plane/envoy/config/core/v3 +go get github.com/gogo/googleapis/google/rpc +go get google.golang.org/grpc +``` + +## Konfiguration ändern + +Wenn die Konfiguration geändert wurde, müssen die Keys zur Validierung wieder erneut gebaut werden. + +``` +go run ./cmd/authelia-gen code keys +``` + +## Bauen + +Um ein Docker Image für authelia zu bauen, müssen die folgenden Befehle ausgeführt werden. + +```sh +# Dieser Befehle funktionieren aktuell nicht +authelia-scripts docker build +authelia-scripts build + +# => Manuell bauen +export CC=musl-gcc + +authelia-scripts build +cp -r dist/public_html internal/server/ +go build -buildmode=pie -ldflags "-linkmode=external -s -w" -trimpath -buildmode=pie -o authelia ./cmd/authelia +mv authelia authelia-linux-amd64-musl +# Build docker image +docker build --tag git.rpjosh.de/rpjosh/authelia/authelia:4.38.0-dev . +docker push git.rpjosh.de/rpjosh/authelia/authelia:4.38.0-dev +# Cleanup +rm -rf internal/server/public_html/ ./authelia-linux-amd64-musl +``` + +# gRCP + +Um einen gRCP Endpunkt nutzen zu können, brauch mein eine *.proto* Datei. Für Envoy sieht diese wie in [dieser Datei](/ext-auth.proto) folgendermaßen aus. \ No newline at end of file diff --git a/config.template.yml b/config.template.yml index f4cedef98..75a5e4f6f 100644 --- a/config.template.yml +++ b/config.template.yml @@ -62,6 +62,10 @@ server: ## This is disabled by default if either /app/.healthcheck.env or /app/healthcheck.sh do not exist. disable_healthcheck: false + ## If a request over the insecure http protocol is received from authelias gRPC endpoint (only for envoy), + ## the request is by default redirected to the matching https URL (301) + disable_autho_https_redirect: false + ## Authelia by default doesn't accept TLS communication on the server port. This section overrides this behaviour. tls: ## The path to the DER base64/PEM format private key. @@ -73,6 +77,15 @@ server: ## The list of certificates for client authentication. client_certificates: [] + ## Enable the support for gRPC ext authentication for envoy. If TLS is enabled in the above section, + ## the defined certificates will also be used for the gRPC endpoint + grpc: + address: 'tcp://:9092' + + # Even if TLS is configured in the server setting (under server.tls), the grcp server won't use TLS + disableTLS: false + + ## Server headers configuration/customization. headers: diff --git a/envoy-proto/ext-auth.proto b/envoy-proto/ext-auth.proto new file mode 100644 index 000000000..1f3ed5787 --- /dev/null +++ b/envoy-proto/ext-auth.proto @@ -0,0 +1,144 @@ +syntax = "proto3"; + +package envoy.service.auth.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/service/auth/v3/attribute_context.proto"; +import "envoy/type/v3/http_status.proto"; + +import "google/protobuf/struct.proto"; +import "google/rpc/status.proto"; + +import "envoy/annotations/deprecation.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.service.auth.v3"; +option java_outer_classname = "ExternalAuthProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3;authv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Authorization service] + +// The authorization service request messages used by external authorization :ref:`network filter +// ` and :ref:`HTTP filter `. + +// A generic interface for performing authorization check on incoming +// requests to a networked service. +service Authorization { + // Performs authorization check based on the attributes associated with the + // incoming request, and returns status `OK` or not `OK`. + rpc Check(CheckRequest) returns (CheckResponse) { + } +} + +message CheckRequest { + option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v2.CheckRequest"; + + // The request attributes. + AttributeContext attributes = 1; +} + +// HTTP attributes for a denied response. +message DeniedHttpResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.DeniedHttpResponse"; + + // This field allows the authorization service to send an HTTP response status code to the + // downstream client. If not set, Envoy sends ``403 Forbidden`` HTTP status code by default. + type.v3.HttpStatus status = 1; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to + // false when used in this message. + repeated config.core.v3.HeaderValueOption headers = 2; + + // This field allows the authorization service to send a response body data + // to the downstream client. + string body = 3; +} + +// HTTP attributes for an OK response. +// [#next-free-field: 9] +message OkHttpResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.OkHttpResponse"; + + // HTTP entity headers in addition to the original request headers. This allows the authorization + // service to append, to add or to override headers from the original request before + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to + // false when used in this message. By setting the ``append`` field to ``true``, + // the filter will append the correspondent header value to the matched request header. + // By leaving ``append`` as false, the filter will either add a new header, or override an existing + // one if there is a match. + repeated config.core.v3.HeaderValueOption headers = 2; + + // HTTP entity headers to remove from the original request before dispatching + // it to the upstream. This allows the authorization service to act on auth + // related headers (like ``Authorization``), process them, and consume them. + // Under this model, the upstream will either receive the request (if it's + // authorized) or not receive it (if it's not), but will not see headers + // containing authorization credentials. + // + // Pseudo headers (such as ``:authority``, ``:method``, ``:path`` etc), as well as + // the header ``Host``, may not be removed as that would make the request + // malformed. If mentioned in ``headers_to_remove`` these special headers will + // be ignored. + // + // When using the HTTP service this must instead be set by the HTTP + // authorization service as a comma separated list like so: + // ``x-envoy-auth-headers-to-remove: one-auth-header, another-auth-header``. + repeated string headers_to_remove = 5; + + // This field has been deprecated in favor of :ref:`CheckResponse.dynamic_metadata + // `. Until it is removed, + // setting this field overrides :ref:`CheckResponse.dynamic_metadata + // `. + google.protobuf.Struct dynamic_metadata = 3 + [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v3.HeaderValueOption response_headers_to_add = 6; + + // This field allows the authorization service to set (and overwrite) query + // string parameters on the original request before it is sent upstream. + repeated config.core.v3.QueryParameter query_parameters_to_set = 7; + + // This field allows the authorization service to specify which query parameters + // should be removed from the original request before it is sent upstream. Each + // element in this list is a case-sensitive query parameter name to be removed. + repeated string query_parameters_to_remove = 8; +} + +// Intended for gRPC and Network Authorization servers ``only``. +message CheckResponse { + option (udpa.annotations.versioning).previous_message_type = + "envoy.service.auth.v2.CheckResponse"; + + // Status ``OK`` allows the request. Any other status indicates the request should be denied, and + // for HTTP filter, if not overridden by :ref:`denied HTTP response status ` + // Envoy sends ``403 Forbidden`` HTTP status code by default. + google.rpc.Status status = 1; + + // An message that contains HTTP response attributes. This message is + // used when the authorization service needs to send custom responses to the + // downstream client or, to modify/add request headers being dispatched to the upstream. + oneof http_response { + // Supplies http attributes for a denied response. + DeniedHttpResponse denied_response = 2; + + // Supplies http attributes for an ok response. + OkHttpResponse ok_response = 3; + } + + // Optional response metadata that will be emitted as dynamic metadata to be consumed by the next + // filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + google.protobuf.Struct dynamic_metadata = 4; +} diff --git a/go.mod b/go.mod index 023f5954f..ce8959ed4 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cristalhq/jwt/v4 v4.0.2 // indirect github.com/dave/jennifer v1.6.0 // indirect @@ -68,12 +69,16 @@ require ( github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/ecordell/optgen v0.0.6 // indirect + github.com/envoyproxy/go-control-plane v0.11.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.1 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-crypt/x v0.2.1 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-webauthn/revoke v0.1.9 // indirect - github.com/golang/glog v1.0.0 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-tpm v0.3.3 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -97,7 +102,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/redis/go-redis/v9 v9.0.4 // indirect @@ -119,12 +124,13 @@ require ( github.com/ysmood/leakless v0.8.0 // indirect golang.org/x/crypto v0.10.0 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.9.0 // indirect - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.8.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect - google.golang.org/grpc v1.54.0 // indirect + google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e // indirect + google.golang.org/grpc v1.56.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7548986e7..9693e9789 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -113,7 +115,11 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.1 h1:kt9FtLiooDc0vbwTLhdg3dyNX1K9Qwa1EK9LcD4jVUQ= +github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk= @@ -154,13 +160,19 @@ github.com/go-webauthn/revoke v0.1.9 h1:gSJ1ckA9VaKA2GN4Ukp+kiGTk1/EXtaDb1YE8Rkn github.com/go-webauthn/revoke v0.1.9/go.mod h1:j6WKPnv0HovtEs++paan9g3ar46gm1NarktkXBaPR+w= github.com/go-webauthn/webauthn v0.5.0 h1:Tbmp37AGIhYbQmcy2hEffo3U3cgPClqvxJ7cLUnF7Rc= github.com/go-webauthn/webauthn v0.5.0/go.mod h1:0CBq/jNfPS9l033j4AxMk8K8MluiMsde9uGNSPFLEVE= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -267,6 +279,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= @@ -363,6 +376,8 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1: github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= @@ -579,6 +594,10 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -707,6 +726,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -716,6 +736,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -724,6 +745,7 @@ golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -793,6 +815,10 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU= google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e h1:Ao9GzfUMPH3zjVfzXG5rlWlk+Q8MXWKwWpwVQE1MXfw= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e h1:NumxXLPfHSndr3wBBdeKiVHjGVFzi9RX2HwwQke94iY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -812,6 +838,10 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= +google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/internal/commands/services.go b/internal/commands/services.go index 10eef7e7d..b4c840058 100644 --- a/internal/commands/services.go +++ b/internal/commands/services.go @@ -16,6 +16,7 @@ import ( "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" "golang.org/x/sync/errgroup" + "google.golang.org/grpc" "github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/server" @@ -33,6 +34,17 @@ func NewServerService(name string, server *fasthttp.Server, listener net.Listene } } +// NewGRCPServerService creates a new ServerService with the appropriate logger etc. +func NewGRCPServerService(name string, server *grpc.Server, listener net.Listener, isTLS bool, log *logrus.Logger) (service *GRCPServerService) { + return &GRCPServerService{ + name: name, + server: server, + listener: listener, + isTLS: isTLS, + log: log.WithFields(map[string]any{logFieldService: serviceTypeServer, serviceTypeServer: name}), + } +} + // NewFileWatcherService creates a new FileWatcherService with the appropriate logger etc. func NewFileWatcherService(name, path string, reload ProviderReload, log *logrus.Logger) (service *FileWatcherService, err error) { if path == "" { @@ -161,6 +173,54 @@ func (service *ServerService) Log() *logrus.Entry { return service.log } +// GRCPServerService is a Service which runs a gRCP server. +type GRCPServerService struct { + name string + server *grpc.Server + isTLS bool + listener net.Listener + log *logrus.Entry +} + +// ServiceType returns the service type for this service, which is always 'server'. +func (service *GRCPServerService) ServiceType() string { + return serviceTypeServer +} + +// ServiceName returns the individual name for this service. +func (service *GRCPServerService) ServiceName() string { + return service.name +} + +// Run the ServerService. +func (service *GRCPServerService) Run() (err error) { + defer func() { + if r := recover(); r != nil { + service.log.WithError(recoverErr(r)).Error("Critical error caught (recovered)") + } + }() + + service.log.Infof(fmtLogServerListening, connectionType(service.isTLS), service.listener.Addr().String()) + + if err = service.server.Serve(service.listener); err != nil { + service.log.WithError(err).Error("Error returned attempting to serve requests") + + return err + } + + return nil +} + +// Shutdown the ServerService. +func (service *GRCPServerService) Shutdown() { + service.server.Stop() +} + +// Log returns the *logrus.Entry of the ServerService. +func (service *GRCPServerService) Log() *logrus.Entry { + return service.log +} + // FileWatcherService is a Service that watches files for changes. type FileWatcherService struct { name string @@ -272,6 +332,19 @@ func svcSvrMetricsFunc(ctx *CmdCtx) (service Service) { return service } +func svcSvrGRPCFunc(ctx *CmdCtx) (service Service) { + switch svr, listener, isTLS, err := server.CreateGRPCServer(ctx.config, ctx.providers); { + case err != nil: + ctx.log.WithError(err).Fatal("Create Server Service (gRPC) returned error") + case svr != nil && listener != nil: + service = NewGRCPServerService("gRCP", svr, listener, isTLS, ctx.log) + default: + ctx.log.Debug("Create Server Service (gRPC) skipped") + } + + return service +} + func svcWatcherUsersFunc(ctx *CmdCtx) (service Service) { var err error @@ -312,7 +385,7 @@ func servicesRun(ctx *CmdCtx) { ) for _, serviceFunc := range []func(ctx *CmdCtx) Service{ - svcSvrMainFunc, svcSvrMetricsFunc, + svcSvrMainFunc, svcSvrGRPCFunc, svcSvrMetricsFunc, svcWatcherUsersFunc, } { if service := serviceFunc(ctx); service != nil { diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index df820a3ce..259bc6568 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -264,6 +264,7 @@ var Keys = []string{ "server.address", "server.asset_path", "server.disable_healthcheck", + "server.disable_autho_https_redirect", "server.tls.certificate", "server.tls.key", "server.tls.client_certificates", @@ -274,6 +275,8 @@ var Keys = []string{ "server.endpoints.authz.*.implementation", "server.endpoints.authz.*.authn_strategies", "server.endpoints.authz.*.authn_strategies[].name", + "server.grpc.address", + "server.grpc.disableTLS", "server.buffers.read", "server.buffers.write", "server.timeouts.read", diff --git a/internal/configuration/schema/server.go b/internal/configuration/schema/server.go index bca2a0b9d..9bdce09ae 100644 --- a/internal/configuration/schema/server.go +++ b/internal/configuration/schema/server.go @@ -7,13 +7,15 @@ import ( // ServerConfiguration represents the configuration of the http server. type ServerConfiguration struct { - Address *AddressTCP `koanf:"address"` - AssetPath string `koanf:"asset_path"` - DisableHealthcheck bool `koanf:"disable_healthcheck"` + Address *AddressTCP `koanf:"address"` + AssetPath string `koanf:"asset_path"` + DisableHealthcheck bool `koanf:"disable_healthcheck"` + DisableAutoHttpsRedirect bool `koanf:"disable_autho_https_redirect"` TLS ServerTLS `koanf:"tls"` Headers ServerHeaders `koanf:"headers"` Endpoints ServerEndpoints `koanf:"endpoints"` + GRPC ServerGRPC `koanf:"grpc"` Buffers ServerBuffers `koanf:"buffers"` Timeouts ServerTimeouts `koanf:"timeouts"` @@ -60,6 +62,15 @@ type ServerHeaders struct { CSPTemplate string `koanf:"csp_template"` } +// ServerGRCP contains configuration options for the gRCP server. +type ServerGRPC struct { + // Address with port to listen on. If this field is empty, no grcp server + // will be spawned. + Address *AddressTCP `koanf:"address"` + + DisableTLS bool `koanf:"disableTLS"` +} + // DefaultServerConfiguration represents the default values of the ServerConfiguration. var DefaultServerConfiguration = ServerConfiguration{ Address: &AddressTCP{Address{true, false, -1, 9091, &url.URL{Scheme: AddressSchemeTCP, Host: ":9091", Path: "/"}}}, diff --git a/internal/handlers/handler_authz_grpc.go b/internal/handlers/handler_authz_grpc.go new file mode 100644 index 000000000..dc5103896 --- /dev/null +++ b/internal/handlers/handler_authz_grpc.go @@ -0,0 +1,399 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net" + "net/url" + + "github.com/authelia/authelia/v4/internal/authorization" + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/middlewares" + "github.com/authelia/authelia/v4/internal/session" + "github.com/authelia/authelia/v4/internal/utils" + core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + autha "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/gogo/googleapis/google/rpc" + "github.com/valyala/fasthttp" + rpcstatus "google.golang.org/genproto/googleapis/rpc/status" + "google.golang.org/grpc/codes" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + + "context" +) + +type AuthzGRCP struct { + Config *schema.Configuration + Providers middlewares.Providers + AuthStrategies []AuthnStrategy + Authz Authz +} + +func NewAuthzGRCP(config *schema.Configuration, providers middlewares.Providers) *AuthzGRCP { + + // Determine the refresh interval + authBuilder := NewAuthzBuilder().WithConfig(config) + + // Only the following strategies are supported. These are hardcoded at the moment and won't be taken from the configuration + strategies := []AuthnStrategy{NewHeaderAuthorizationAuthnStrategy(), NewCookieSessionAuthnStrategy(authBuilder.config.RefreshInterval)} + + return &AuthzGRCP{ + Config: config, + Providers: providers, + AuthStrategies: strategies, + Authz: Authz{strategies: strategies}, + } +} + +type GRCPRequestData struct { + RemoteHost string + Domain string + Method string + Protocol string + Path string + AutheliaURL string +} + +func (data *GRCPRequestData) String() string { + return fmt.Sprintf( + `Extracted data from headers: +Remote Host = %s +Domain = %s +Method = %s +Protocol = %s +Path = %s +AutheliaURL = %s +`, data.RemoteHost, data.Domain, data.Method, data.Protocol, data.Path, data.AutheliaURL) +} + +// GetRequestURI returns a request URI from the provided Data inside this struct. +// It returns an error if values are missing +func (data *GRCPRequestData) GetRequestURI() (*url.URL, error) { + if len(data.Protocol) == 0 || len(data.Domain) == 0 || len(data.Path) == 0 || len(data.Method) == 0 { + return nil, fmt.Errorf("missing required values for URI. One of 'Protocol', 'Domain', 'Path' or 'Method' was not found") + } + + return url.ParseRequestURI(fmt.Sprintf("%s://%s%s", data.Protocol, data.Domain, data.Path)) +} + +// GetAutheliaURI returns a URI for the authelia protal from the provided Data inside this struct. +func (data *GRCPRequestData) GetAutheliaURI(provider *session.Session) (*url.URL, error) { + if len(data.AutheliaURL) == 0 { + return nil, fmt.Errorf("missing required 'AutheliaURL' for request") + } + + // Parse the URL + uri, err := url.ParseRequestURI(data.AutheliaURL) + if err != nil { + return nil, err + } + + // Validate URL + if !utils.HasURIDomainSuffix(uri, provider.Config.Domain) { + return nil, fmt.Errorf("authelia url '%s' is not valid for detected domain '%s' as the url does not have the domain as a suffix", data.AutheliaURL, provider.Config.Domain) + } + + return uri, nil +} + +// HandleGRPC is the authentication handler for envoy proxies via the gRPC protocoll. +// It is oriented on handler_authz.go +func (authz *AuthzGRCP) Check(goCtx context.Context, req *autha.CheckRequest) (*autha.CheckResponse, error) { + request, data := authz.GetHttpCtxFromGRPC(req) + ctx := middlewares.NewAutheliaCtx(request, *authz.Config, authz.Providers) + + // Log the parsed data and the inbound headers + ctx.Logger.Debug(data) + b, err := json.MarshalIndent(req.Attributes.Request.Http.Headers, "", " ") + if err == nil { + ctx.Logger.Trace("Inbound Headers: ") + ctx.Logger.Trace((string(b))) + } + + // Get request URI + uri, err := data.GetRequestURI() + if err != nil { + ctx.Logger.WithError(err).Error("Error getting Target URL and Request Method") + return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad authentication request"), nil + } + + // Get authentication object + object := authorization.NewObject(uri, data.Method) + if !utils.IsURISecure(object.URL) { + // Check for redirect + if !ctx.Configuration.Server.DisableAutoHttpsRedirect { + // Redirect to https + return authz.AuthResponseHeader(envoy_type.StatusCode_PermanentRedirect, "", []*core.HeaderValueOption{ + { + Header: &core.HeaderValue{ + Key: "Location", + Value: fmt.Sprintf("https://%s%s", data.Domain, data.Path), + }, + }, + }), nil + } + ctx.Logger.Errorf("Target URL '%s' has an insecure scheme '%s', only the 'https' and 'wss' schemes are supported so session cookies can be transmitted securely", object.URL.String(), object.URL.Scheme) + return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad protocol for authentication request"), nil + } + + // Get provider + provider, err := ctx.GetSessionProviderByTargetURL(object.URL) + if err != nil { + ctx.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Target URL does not appear to have a relevant session cookies configuration") + return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad domain for authentication request"), nil + } + + // Get authelia url + if len(data.AutheliaURL) == 0 { + ctx.Logger.Info("Received no authelia URL from headers. Using the default of https://auth.rpjosh.de") + data.AutheliaURL = "https://auth.rpjosh.de" + } + autheliaURI, err := data.GetAutheliaURI(provider) + if err != nil { + ctx.Logger.WithError(err).WithField("target_url", object.URL.String()).Error("Error occurred trying to determine the external Authelia URL for Target URL") + return authz.ErrAuthResponse(envoy_type.StatusCode_BadRequest, "Bad authentication request"), nil + } + ctx.Logger.Trace("Using authelia URL " + autheliaURI.String()) + + var ( + authn Authn + strategy AuthnStrategy + ) + + // Get strategie + if authn, strategy, err = authz.Authz.authn(ctx, provider); err != nil { + authn.Object = object + + ctx.Logger.WithError(err).Error("Error occurred while attempting to authenticate a request") + + switch strategy { + case nil: + ctx.Logger.Trace("Received no strategy to use") + return authz.ErrAuthResponse(envoy_type.StatusCode_Unauthorized, "Unauthorized"), nil + default: + // Because the response is modified directly, we have to catch this and rewrite this function + if s, ok := strategy.(*HeaderAuthnStrategy); ok { + ctx.Logger.Trace("Rewriting HeaderAuthnStrategy") + ctx.Logger.Debugf("Responding %d %s", s.authn, autheliaURI) + + headers := make([]*core.HeaderValueOption, 0) + if s.headerAuthenticate != nil { + headers = append(headers, &core.HeaderValueOption{ + Header: &core.HeaderValue{ + Key: string(s.headerAuthenticate), + Value: string(headerValueAuthenticateBasic), + }, + }) + } + + return authz.AuthResponseHeader(envoy_type.StatusCode(s.statusAuthenticate), "", headers), nil + } else if _, ok := strategy.(*CookieSessionAuthnStrategy); ok { + ctx.Logger.Trace("Rewriting CookieSessionAuthnStrategy") + + } + + ctx.Logger.Error("Received unsupported auth strategy for gRPC server") + // strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)) + } + + return authz.ErrAuthResponse(envoy_type.StatusCode_Unauthorized, "Unauthorized"), nil + } + + authn.Object = object + authn.Method = friendlyMethod(authn.Object.Method) + + ruleHasSubject, required := ctx.Providers.Authorizer.GetRequiredLevel( + authorization.Subject{ + Username: authn.Details.Username, + Groups: authn.Details.Groups, + IP: net.ParseIP(data.RemoteHost), + }, + object, + ) + + switch isAuthzResult(authn.Level, required, ruleHasSubject) { + case AuthzResultForbidden: + ctx.Logger.Infof("Access to '%s' is forbidden to user '%s'", object.URL.String(), authn.Username) + return authz.ErrAuthResponse(envoy_type.StatusCode_Forbidden, "Forbidden"), nil + case AuthzResultUnauthorized: + if strategy != nil { + ctx.Logger.Error("Handling not supported handler") + //strategy.HandleUnauthorized(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)) + } else { + ctx.Logger.Debugf("Redirecting user") + return authz.HandleUnauthorizedRedirect(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)), nil + } + case AuthzResultAuthorized: + ctx.Logger.Debugf("Authorized request") + return authz.HandleAuthorized(ctx, &authn), nil + //authz.Authz.handleAuthorized(ctx, &authn) + } + + ctx.Logger.Info("Handling default redirection") + return authz.HandleUnauthorizedRedirect(ctx, &authn, authz.Authz.getRedirectionURL(&object, autheliaURI)), nil +} + +// GetHttpCtxFromGRPC is an Adapter that converts common fields between a grpc request +// a "normal" http request. +// It also returns the +func (authz *AuthzGRCP) GetHttpCtxFromGRPC(req *autha.CheckRequest) (*fasthttp.RequestCtx, *GRCPRequestData) { + + // Extract headers + headers := req.Attributes.Request.Http.Headers + + // Parse the header data + data := &GRCPRequestData{ + RemoteHost: headers["x-forwarded-for"], + Domain: headers[":authority"], + Method: headers[":method"], + Protocol: headers["x-forwarded-proto"], + Path: headers[":path"], + AutheliaURL: headers[""], + } + + // Build fasthttp request with common types + rtc := &fasthttp.RequestCtx{} + + // General headers + rtc.Request.Header.Set(fasthttp.HeaderXForwardedFor, data.RemoteHost) + + // Needed for NewHeaderProxyAuthorizationAuthnStrategy and NewHeaderAuthorizationAuthnStrategy + if val, isSet := headers["authorization"]; isSet { + rtc.Request.Header.Set(fasthttp.HeaderAuthorization, val) + } + rtc.Request.Header.Set(fasthttp.HeaderProxyAuthorization, headers[fasthttp.HeaderProxyAuthorization]) + rtc.Request.Header.Set(fasthttp.HeaderWWWAuthenticate, headers[fasthttp.HeaderWWWAuthenticate]) + + // Needed for CookieSesseionauthnStrategy + rtc.Request.Header.Set("cookie", headers["cookie"]) + + return rtc, data +} + +// ErrAuthResponse returns an authentication error for envoy with the given status code +// and the given text body +func (authz *AuthzGRCP) ErrAuthResponse(statuscode envoy_type.StatusCode, body string) *autha.CheckResponse { + return &autha.CheckResponse{ + Status: &rpcstatus.Status{ + Code: int32(rpc.UNAUTHENTICATED), + }, + HttpResponse: &autha.CheckResponse_DeniedResponse{ + DeniedResponse: &autha.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{ + Code: statuscode, + }, + Body: body, + }, + }, + } +} + +// ErrAuthResponse returns an authentication error for envoy with the given status code +// and the given text body +func (authz *AuthzGRCP) AuthResponseHeader(statuscode envoy_type.StatusCode, body string, headers []*core.HeaderValueOption) *autha.CheckResponse { + return &autha.CheckResponse{ + Status: &rpcstatus.Status{ + Code: int32(rpc.UNAUTHENTICATED), + }, + HttpResponse: &autha.CheckResponse_DeniedResponse{ + DeniedResponse: &autha.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{ + Code: statuscode, + }, + Body: body, + Headers: headers, + }, + }, + } +} + +func (authz *AuthzGRCP) HandleAuthorized(ctx *middlewares.AutheliaCtx, authn *Authn) *autha.CheckResponse { + return &autha.CheckResponse{ + Status: &rpcstatus.Status{ + Code: int32(rpc.OK), + }, + HttpResponse: &autha.CheckResponse_OkResponse{ + OkResponse: &autha.OkHttpResponse{ + Headers: []*core.HeaderValueOption{ + { + Header: &core.HeaderValue{ + Key: "Auth-Handler", + Value: getAuthType(authn.Type), + }, + }, + { + Header: &core.HeaderValue{ + Key: "Auth-Username", + Value: authn.Username, + }, + }, + { + Header: &core.HeaderValue{ + Key: "Auth-Username-Display", + Value: authn.Details.DisplayName, + }, + }, + { + Header: &core.HeaderValue{ + Key: "Auth-Realm", + Value: authn.Object.Domain, + }, + }, + }, + }, + }, + } +} + +func (authz *AuthzGRCP) HandleUnauthorizedRedirect(ctx *middlewares.AutheliaCtx, authn *Authn, redirectionURL *url.URL) *autha.CheckResponse { + return &autha.CheckResponse{ + Status: &rpcstatus.Status{ + Code: int32(rpc.UNAUTHENTICATED), + }, + HttpResponse: &autha.CheckResponse_DeniedResponse{ + DeniedResponse: &autha.DeniedHttpResponse{ + Status: &envoy_type.HttpStatus{ + Code: 302, + }, + Headers: []*core.HeaderValueOption{ + { + Header: &core.HeaderValue{ + Key: "Location", + Value: redirectionURL.String(), + }, + }, + }, + }, + }, + } +} + +func getAuthType(t AuthnType) string { + + switch t { + case AuthnTypeNone: + return "none" + case AuthnTypeCookie: + return "cookie" + case AuthnTypeProxyAuthorization: + return "proxy" + case AuthnTypeAuthorization: + return "header" + default: + return "unknown" + } +} + +// HealthEndpoint implements a health function to check if the appliction is +// still alive +type HealthEndpoint struct{} + +func (s *HealthEndpoint) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil +} + +func (s *HealthEndpoint) Watch(in *healthpb.HealthCheckRequest, srv healthpb.Health_WatchServer) error { + return status.Error(codes.Unimplemented, "Watch is not implemented") +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index cc7674067..01eb26b40 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -9,6 +9,7 @@ import ( "time" duoapi "github.com/duosecurity/duo_api_golang" + autha "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" "github.com/fasthttp/router" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" @@ -16,6 +17,8 @@ import ( "github.com/valyala/fasthttp/expvarhandler" "github.com/valyala/fasthttp/fasthttpadaptor" "github.com/valyala/fasthttp/pprofhandler" + "google.golang.org/grpc" + healthpb "google.golang.org/grpc/health/grpc_health_v1" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/duo" @@ -189,7 +192,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) uri := path.Join(pathAuthz, name) authz := handlers.NewAuthzBuilder().WithConfig(config).WithEndpointConfig(endpoint).Build() - + // @HERE handler := middlewares.Wrap(metricsVRMW, bridge(authz.Handler)) switch name { @@ -404,6 +407,12 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers) return handler } +func handleGRCP(server *grpc.Server, config *schema.Configuration, providers middlewares.Providers) { + // Register endpoints + healthpb.RegisterHealthServer(server, &handlers.HealthEndpoint{}) + autha.RegisterAuthorizationServer(server, handlers.NewAuthzGRCP(config, providers)) +} + func handleMetrics(path string) fasthttp.RequestHandler { r := router.New() diff --git a/internal/server/server.go b/internal/server/server.go index f76e6cfc3..afdbaf7a0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,6 +9,8 @@ import ( "github.com/sirupsen/logrus" "github.com/valyala/fasthttp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/logging" @@ -44,30 +46,13 @@ func CreateDefaultServer(config *schema.Configuration, providers middlewares.Pro if config.Server.TLS.Certificate != "" && config.Server.TLS.Key != "" { isTLS, connectionScheme = true, schemeHTTPS - if err = server.AppendCert(config.Server.TLS.Certificate, config.Server.TLS.Key); err != nil { - return nil, nil, nil, false, fmt.Errorf("unable to load tls server certificate '%s' or private key '%s': %w", config.Server.TLS.Certificate, config.Server.TLS.Key, err) + tlsConfig, err := loadTLSCertificates(config) + if err != nil { + return nil, nil, nil, false, fmt.Errorf("HTTP server: %w", err) } - if len(config.Server.TLS.ClientCertificates) > 0 { - caCertPool := x509.NewCertPool() - - var cert []byte - - for _, path := range config.Server.TLS.ClientCertificates { - if cert, err = os.ReadFile(path); err != nil { - return nil, nil, nil, false, fmt.Errorf("unable to load tls client certificate '%s': %w", path, err) - } - - caCertPool.AppendCertsFromPEM(cert) - } - - // ClientCAs should never be nil, otherwise the system cert pool is used for client authentication - // but we don't want everybody on the Internet to be able to authenticate. - server.TLSConfig.ClientCAs = caCertPool - server.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert - } - - listener = tls.NewListener(listener, server.TLSConfig.Clone()) + // Apply configuration + listener = tls.NewListener(listener, tlsConfig.Clone()) } if err = writeHealthCheckEnv(config.Server.DisableHealthcheck, connectionScheme, config.Server.Address.Hostname(), @@ -108,3 +93,72 @@ func CreateMetricsServer(config *schema.Configuration, providers middlewares.Pro return server, listener, []string{config.Telemetry.Metrics.Address.Path()}, false, nil } + +// CreateGRPCServer creates a server for handling gRPC authentication requests from an envoy proxy. +func CreateGRPCServer(config *schema.Configuration, providers middlewares.Providers) (server *grpc.Server, listener net.Listener, tls bool, err error) { + if config.Server.GRPC.Address == nil { + return nil, nil, false, nil + } + + // Initialize gPRC server + lis, err := config.Server.GRPC.Address.Listener() + if err != nil { + return nil, nil, false, fmt.Errorf("error occurred while attempting to initialize grcp server listener for address '%s': %w", config.Server.GRPC.Address, err) + } + + opts := []grpc.ServerOption{grpc.MaxConcurrentStreams(10)} + + if config.Server.TLS.Certificate != "" && config.Server.TLS.Key != "" && !config.Server.GRPC.DisableTLS { + tlsConfig, err := loadTLSCertificates(config) + if err != nil { + return nil, nil, false, fmt.Errorf("gRPC server: %w", err) + } + + opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConfig))) + tls = true + } + + s := grpc.NewServer(opts...) + handleGRCP(s, config, providers) + + return s, lis, tls, nil +} + +// loadTLSCertificates loads the server and client certificates from the files and returns a +// tls.Config object for the server +func loadTLSCertificates(config *schema.Configuration) (*tls.Config, error) { + + // Load the server certificates + serverCert, err := tls.LoadX509KeyPair(config.Server.TLS.Certificate, config.Server.TLS.Key) + if err != nil { + return nil, fmt.Errorf("unable to load server certificate '%s' or key '%s': %w", config.Server.TLS.Certificate, config.Server.TLS.Key, err) + } + + // Create the tls configuration + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + ClientAuth: tls.NoClientCert, + } + + // Load client certificates + if len(config.Server.TLS.ClientCertificates) > 0 { + caCertPool := x509.NewCertPool() + + var cert []byte + + for _, path := range config.Server.TLS.ClientCertificates { + if cert, err = os.ReadFile(path); err != nil { + return nil, fmt.Errorf("unable to load tls client certificate '%s': %w", path, err) + } + + caCertPool.AppendCertsFromPEM(cert) + } + + // ClientCAs should never be nil, otherwise the system cert pool is used for client authentication, + // but we don't want everybody on the Internet to be able to authenticate. + tlsConfig.ClientCAs = caCertPool + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + } + + return tlsConfig, nil +} diff --git a/internal/suites/Standalone/configuration.yml b/internal/suites/Standalone/configuration.yml index 255afeeae..f0a122953 100644 --- a/internal/suites/Standalone/configuration.yml +++ b/internal/suites/Standalone/configuration.yml @@ -10,6 +10,9 @@ server: tls: certificate: /pki/public.backend.crt key: /pki/private.backend.pem + grpc: + address: 'tcp://0.0.0.0:9092' + disableTLS: false telemetry: metrics: @@ -17,7 +20,7 @@ telemetry: address: tcp://0.0.0.0:9959 log: - level: debug + level: trace authentication_backend: file: @@ -30,6 +33,8 @@ session: cookies: - domain: 'example.com' authelia_url: 'https://login.example.com:8080' + - domain: 'rpjosh.de' + authelia_url: 'https://ubuntugui.rpjosh.de:9091' storage: encryption_key: a_not_so_secure_encryption_key @@ -83,6 +88,18 @@ access_control: subject: "user:bob" policy: two_factor + - domain: auth.rpjosh.de + policy: bypass + + - domain: datenbank.rpjosh.de + resources: + - "^/$" + policy: bypass + - domain: datenbank.rpjosh.de + resources: + - "^/hi.*$" + policy: one_factor + regulation: # Set it to 0 to disable max_retries. diff --git a/internal/suites/example/compose/authelia/Dockerfile.backend b/internal/suites/example/compose/authelia/Dockerfile.backend index a1db3bf75..3dce4aff9 100644 --- a/internal/suites/example/compose/authelia/Dockerfile.backend +++ b/internal/suites/example/compose/authelia/Dockerfile.backend @@ -17,3 +17,4 @@ ENV PATH="/app:${PATH}" \ VOLUME /config EXPOSE 9091 +EXPOSE 9092 \ No newline at end of file diff --git a/internal/suites/example/compose/authelia/resources/entrypoint-frontend.sh b/internal/suites/example/compose/authelia/resources/entrypoint-frontend.sh index 880337a8e..2bb065a09 100755 --- a/internal/suites/example/compose/authelia/resources/entrypoint-frontend.sh +++ b/internal/suites/example/compose/authelia/resources/entrypoint-frontend.sh @@ -2,4 +2,4 @@ set -x -pnpm install --frozen-lockfile && pnpm start \ No newline at end of file +pnpm start \ No newline at end of file