From 60396f837e50edf820c8a315ef15312366f6890b Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Fri, 7 Jul 2023 18:47:04 -0400 Subject: [PATCH] chore: decommission traefik, auth-service (#23) --- services/auth-service/Dockerfile | 15 -- services/auth-service/cmd/authservice.go | 106 -------------- services/auth-service/docker-compose.yml | 18 --- services/auth-service/go.mod | 8 -- services/auth-service/go.sum | 4 - .../auth-service/internal/helpers/cache.go | 55 -------- .../internal/helpers/configuration.go | 49 ------- .../internal/httpUtils/httpHandler.go | 57 -------- .../internal/httpUtils/middlewares.go | 51 ------- services/auth-service/internal/oauth/oauth.go | 130 ------------------ services/traefik/.env | 1 - services/traefik/.gitignore | 1 - services/traefik/README.md | 15 -- services/traefik/docker-compose.yml | 18 --- services/traefik/traefik.toml | 28 ---- services/traefik/traefik_dynamic.toml | 54 -------- services/traefik/traefik_dynamic_local.toml | 53 ------- 17 files changed, 663 deletions(-) delete mode 100644 services/auth-service/Dockerfile delete mode 100644 services/auth-service/cmd/authservice.go delete mode 100644 services/auth-service/docker-compose.yml delete mode 100644 services/auth-service/go.mod delete mode 100644 services/auth-service/go.sum delete mode 100644 services/auth-service/internal/helpers/cache.go delete mode 100644 services/auth-service/internal/helpers/configuration.go delete mode 100644 services/auth-service/internal/httpUtils/httpHandler.go delete mode 100644 services/auth-service/internal/httpUtils/middlewares.go delete mode 100644 services/auth-service/internal/oauth/oauth.go delete mode 100644 services/traefik/.env delete mode 100644 services/traefik/.gitignore delete mode 100644 services/traefik/README.md delete mode 100644 services/traefik/docker-compose.yml delete mode 100644 services/traefik/traefik.toml delete mode 100644 services/traefik/traefik_dynamic.toml delete mode 100644 services/traefik/traefik_dynamic_local.toml diff --git a/services/auth-service/Dockerfile b/services/auth-service/Dockerfile deleted file mode 100644 index 9660476..0000000 --- a/services/auth-service/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM golang:1.20 - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY ./cmd ./cmd -COPY ./internal ./internal - -RUN go build ./cmd/authservice.go - -EXPOSE 8080 - -CMD ["./authservice", "./config.json"] diff --git a/services/auth-service/cmd/authservice.go b/services/auth-service/cmd/authservice.go deleted file mode 100644 index ff17ea4..0000000 --- a/services/auth-service/cmd/authservice.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "authservice/internal/helpers" - "authservice/internal/httpUtils" - "authservice/internal/oauth" - "fmt" - "log" - "net/http" - "net/url" - "os" -) - -// Healthcheck endpoint -// -// Responds to pings with a 200, quick way to verify -// if the service is up and can respond to requests. -func healthCheck(request *http.Request) httpUtils.HttpResponse { - return httpUtils.HttpResponse{Body: "Up!", StatusCode: 200} -} - -func login(request *http.Request) httpUtils.HttpResponse { - if request.Context().Value("user") != nil { - return httpUtils.HttpResponse{StatusCode: 200} - } - - configuration := helpers.GetConfiguration() - requestId := request.Context().Value("requestId").(string) - queryParameters := url.Values{ - "client_id": {os.Getenv("CLIENT_ID")}, - "response_type": {"code"}, - "scope": {"https://www.googleapis.com/auth/userinfo.email"}, - "redirect_uri": {configuration.OAuth.CallbackUrl}, - "state": {requestId}, - } - - redirectUrl, _ := url.Parse(configuration.OAuth.Url) - redirectUrl.RawQuery = queryParameters.Encode() - - intendedHost := request.Header["X-Forwarded-Host"] - intendedDestination := request.Header["X-Forwarded-Uri"] - - if len(intendedDestination) == 1 { - helpers.SetCachedValue(fmt.Sprintf("postlogin_redir_%s", requestId), fmt.Sprintf("https://%s%s", intendedHost[0], intendedDestination[0]), 60) - } - - return httpUtils.HttpResponse{StatusCode: 302, RedirectTo: redirectUrl.String()} -} - -func oauthRedirect(request *http.Request) httpUtils.HttpResponse { - configuration := helpers.GetConfiguration() - requestId := request.URL.Query()["state"][0] - intendedDestination, hasIntendedDestination := helpers.GetCachedValue(fmt.Sprintf("postlogin_redir_%s", requestId)) - queryParameters, _ := url.ParseQuery(request.URL.RawQuery) - - code, _ := url.QueryUnescape(queryParameters["code"][0]) - - token, tokenError := oauth.GetTokenFromProvider(code) - - if tokenError != nil { - return httpUtils.HttpResponse{StatusCode: 400, Body: "Failed to get token from oauth"} - } - - log.Println(fmt.Sprintf("Received token: %s", token.IdToken)) - cookie := http.Cookie{Name: "jwt", Value: token.IdToken, Domain: configuration.Hostname, Path: "/"} - if hasIntendedDestination { - return httpUtils.HttpResponse{StatusCode: 302, RedirectTo: intendedDestination, Cookies: []http.Cookie{cookie}} - } - - return httpUtils.HttpResponse{StatusCode: 200, Cookies: []http.Cookie{cookie}} -} - -func main() { - configurationPath := os.Args[1] - - configuration, err := helpers.LoadConfiguration(configurationPath) - - helpers.LoadedConfiguration = &configuration - - if err != nil { - log.Fatal(err) - } - - mux := http.NewServeMux() - host := ":8080" - - routes := map[string](func(*http.Request) httpUtils.HttpResponse){ - "/": healthCheck, - "/auth/callback": oauthRedirect, - "/auth/login": login, - } - - for path, handler := range routes { - mux.HandleFunc(path, httpUtils.MakeHandler(handler)) - } - - log.Println(fmt.Sprintf("Listening on %s", host)) - - err = http.ListenAndServe(host, mux) - - if err != nil { - log.Fatal(err) - } else { - log.Println("Exiting") - } -} diff --git a/services/auth-service/docker-compose.yml b/services/auth-service/docker-compose.yml deleted file mode 100644 index 7f807e9..0000000 --- a/services/auth-service/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.7' - -services: - auth-service: - restart: always - build: . - env_file: - - .env - ports: - - 8080:8080 - volumes: - - ./config.json:/app/config.json - -networks: - default: - name: web - external: true - diff --git a/services/auth-service/go.mod b/services/auth-service/go.mod deleted file mode 100644 index f3ebba6..0000000 --- a/services/auth-service/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module authservice - -go 1.20 - -require ( - github.com/golang-jwt/jwt/v5 v5.0.0 - github.com/google/uuid v1.3.0 -) diff --git a/services/auth-service/go.sum b/services/auth-service/go.sum deleted file mode 100644 index 80c8498..0000000 --- a/services/auth-service/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/services/auth-service/internal/helpers/cache.go b/services/auth-service/internal/helpers/cache.go deleted file mode 100644 index 7789a03..0000000 --- a/services/auth-service/internal/helpers/cache.go +++ /dev/null @@ -1,55 +0,0 @@ -package helpers - -import ( - "fmt" - "log" - "time" -) - -type CacheEntry struct { - Value string - Expiration int64 -} - -var InMemoryCache *map[string]CacheEntry - -func getCache() map[string]CacheEntry { - if InMemoryCache == nil { - InMemoryCache = &map[string]CacheEntry{} - } - - return *InMemoryCache -} - -func pruneExpiredEntries() int { - now := time.Now().Unix() - - pruned := 0 - for key, value := range getCache() { - if value.Expiration < now { - delete(*InMemoryCache, key) - pruned += 1 - } - } - - return pruned -} - -func GetCachedValue(key string) (string, bool) { - pruneExpiredEntries() - - entry, ok := getCache()[key] - - log.Println(fmt.Sprintf("[Cache] Retrieved %s=%s from cache", key, entry.Value)) - - return entry.Value, ok -} - -func SetCachedValue(key string, value string, timeToLive int) { - pruneExpiredEntries() - - expiration := time.Now().Unix() + int64(timeToLive) - log.Println(fmt.Sprintf("[Cache] Created or updated %s=%s (exp=%d) in cache", key, value, expiration)) - - getCache()[key] = CacheEntry{Value: value, Expiration: expiration} -} diff --git a/services/auth-service/internal/helpers/configuration.go b/services/auth-service/internal/helpers/configuration.go deleted file mode 100644 index bbcad72..0000000 --- a/services/auth-service/internal/helpers/configuration.go +++ /dev/null @@ -1,49 +0,0 @@ -package helpers - -import ( - "encoding/json" - "io/ioutil" - "log" -) - -var LoadedConfiguration *Configuration - -type OAuthConfiguration struct { - Url string `json:"url"` - TokenUrl string `json:"token_url"` - PublicKeyUrl string `json:"public_key_url"` - CallbackUrl string `json:"callback_url"` - Issuer string `json:"issuer"` -} - -type Configuration struct { - VerifiedUsers []string `json:"verified_users"` - Hostname string `json:"hostname"` - OAuth OAuthConfiguration `json:"oauth_configuration"` -} - -func GetConfiguration() *Configuration { - if LoadedConfiguration == nil { - log.Fatal("Configuration file has not been loaded yet.") - } - - return LoadedConfiguration -} - -func LoadConfiguration(configPath string) (Configuration, error) { - file, fileError := ioutil.ReadFile(configPath) - - if fileError != nil { - return Configuration{}, fileError - } - - var parsedConfiguration Configuration - - jsonError := json.Unmarshal(file, &parsedConfiguration) - - if jsonError != nil { - return Configuration{}, jsonError - } - - return parsedConfiguration, nil -} diff --git a/services/auth-service/internal/httpUtils/httpHandler.go b/services/auth-service/internal/httpUtils/httpHandler.go deleted file mode 100644 index 8852ae2..0000000 --- a/services/auth-service/internal/httpUtils/httpHandler.go +++ /dev/null @@ -1,57 +0,0 @@ -package httpUtils - -import ( - "fmt" - "log" - "net/http" -) - -type HttpResponse struct { - Body string - StatusCode int - Headers map[string]string - Cookies []http.Cookie - RedirectTo string - Authorization string -} - -// Wraps handlers and abstracts response writing away from HTTP handlers themselves. -// -// This expects handlers to return a HttpResponse struct and handles basic logging -// before finishing the request handling. This should wrap all handlers passed to HandlerFunc. -func MakeHandler(handler func(*http.Request) HttpResponse) func(http.ResponseWriter, *http.Request) { - return func(responseWriter http.ResponseWriter, request *http.Request) { - // The authorization middleware will handle validating tokens included - // in cookies or Authorization headers. If a token is present and valid, - // a `user` context value is added to the request context, otherwise - // it is nil. - enhancedRequest, err := AuthorizationMiddleware(request) - enhancedRequest, _ = RequestIdMiddleware(enhancedRequest) - - var response HttpResponse - - if err != nil { - response = HttpResponse{StatusCode: 401} - } else { - response = handler(enhancedRequest) - } - - defer log.Println(fmt.Sprintf("%s %s - %d", request.Method, request.URL, response.StatusCode)) - - for _, cookie := range response.Cookies { - http.SetCookie(responseWriter, &cookie) - } - - for key, value := range response.Headers { - responseWriter.Header().Set(key, value) - } - - if response.StatusCode == 302 { - http.Redirect(responseWriter, request, response.RedirectTo, 302) - } else if response.StatusCode > 100 && response.StatusCode != 200 { - responseWriter.WriteHeader(response.StatusCode) - } - - responseWriter.Write([]byte(response.Body)) - } -} diff --git a/services/auth-service/internal/httpUtils/middlewares.go b/services/auth-service/internal/httpUtils/middlewares.go deleted file mode 100644 index 57004a3..0000000 --- a/services/auth-service/internal/httpUtils/middlewares.go +++ /dev/null @@ -1,51 +0,0 @@ -package httpUtils - -import ( - "authservice/internal/oauth" - "context" - "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" - "net/http" -) - -type UserContext struct { - Token string - Email string - Verified bool -} - -func RequestIdMiddleware(request *http.Request) (*http.Request, error) { - requestId := uuid.New().String() - - requestCtx := context.WithValue(request.Context(), "requestId", requestId) - - return request.WithContext(requestCtx), nil -} - -func AuthorizationMiddleware(request *http.Request) (*http.Request, error) { - authorizationHeader := request.Header["Authorization"] - jwtCookie, _ := request.Cookie("jwt") - - var token string - - if len(authorizationHeader) != 0 { - token = authorizationHeader[0] - } else if jwtCookie != nil { - token = jwtCookie.Value - } - - if len(token) == 0 { - return request, nil - } - - decodedToken, err := oauth.AuthorizeToken(token) - - if err != nil { - return request, err - } - - userContext := UserContext{Token: token, Email: decodedToken.Claims.(jwt.MapClaims)["email"].(string), Verified: decodedToken.Claims.(jwt.MapClaims)["email_verified"].(bool)} - requestCtx := context.WithValue(request.Context(), "user", userContext) - - return request.WithContext(requestCtx), nil -} diff --git a/services/auth-service/internal/oauth/oauth.go b/services/auth-service/internal/oauth/oauth.go deleted file mode 100644 index a2ab691..0000000 --- a/services/auth-service/internal/oauth/oauth.go +++ /dev/null @@ -1,130 +0,0 @@ -package oauth - -import ( - "authservice/internal/helpers" - "encoding/json" - "fmt" - "github.com/golang-jwt/jwt/v5" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "strings" -) - -type AccessToken struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - RefreshToken string `json:"refresh_token"` - IdToken string `json:"id_token"` -} - -func getPublicKeysFromIssuer(publicKeyUrl string) (map[string]string, error) { - response, requestError := http.Get(publicKeyUrl) - - if requestError != nil { - return map[string]string{}, requestError - } - - responseBody, responseBodyError := ioutil.ReadAll(response.Body) - - if responseBodyError != nil { - return map[string]string{}, responseBodyError - } - - publicKeys := map[string]string{} - - jsonError := json.Unmarshal(responseBody, &publicKeys) - - if jsonError != nil { - return map[string]string{}, jsonError - } - - return publicKeys, nil -} - -func GetTokenFromProvider(code string) (AccessToken, error) { - configuration := helpers.GetConfiguration() - - requestData := url.Values{ - "code": {code}, - "client_id": {os.Getenv("CLIENT_ID")}, - "client_secret": {os.Getenv("CLIENT_SECRET")}, - "redirect_uri": {configuration.OAuth.CallbackUrl}, - "grant_type": {"authorization_code"}, - } - - response, requestError := http.Post(configuration.OAuth.TokenUrl, "application/x-www-form-urlencoded", strings.NewReader(requestData.Encode())) - - if requestError != nil { - return AccessToken{}, requestError - } - - responseBody, responseBodyError := ioutil.ReadAll(response.Body) - - if responseBodyError != nil { - return AccessToken{}, responseBodyError - } - - retrievedToken := AccessToken{} - - jsonError := json.Unmarshal(responseBody, &retrievedToken) - - if jsonError != nil { - return AccessToken{}, jsonError - } - - return retrievedToken, nil -} - -func AuthorizeToken(token string) (*jwt.Token, error) { - configuration := helpers.GetConfiguration() - - publicKeys, publicKeyError := getPublicKeysFromIssuer(configuration.OAuth.PublicKeyUrl) - - if publicKeyError != nil { - log.Println(fmt.Sprintf("ERROR! %s", publicKeyError)) - return nil, publicKeyError - } - - decodedToken, decodeError := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - claims := token.Claims.(jwt.MapClaims) - - issuer, _ := claims.GetIssuer() - audience, _ := claims.GetAudience() - - if audience[0] != os.Getenv("CLIENT_ID") { - return nil, fmt.Errorf("Invalid claim did not match client ID: %v", claims["aud"]) - } - - if issuer != configuration.OAuth.Issuer { - return nil, fmt.Errorf("Invalid claim did not match issuer: %v", claims["iss"]) - } - - tokenKeyId := token.Header["kid"].(string) - - return jwt.ParseRSAPublicKeyFromPEM([]byte(publicKeys[tokenKeyId])) - }) - - if decodeError != nil { - log.Println(fmt.Sprintf("ERROR! %s", decodeError)) - return decodedToken, decodeError - } - - verified := false - for _, verifiedUser := range configuration.VerifiedUsers { - claims := decodedToken.Claims.(jwt.MapClaims) - if verifiedUser == claims["email"].(string) && claims["email_verified"].(bool) { - verified = true - } - } - - if !verified { - return decodedToken, fmt.Errorf("Forbidden") - } - - return decodedToken, nil -} diff --git a/services/traefik/.env b/services/traefik/.env deleted file mode 100644 index 110d645..0000000 --- a/services/traefik/.env +++ /dev/null @@ -1 +0,0 @@ -CONFIG_ROOT=${APP_STORAGE_ROOT:-.}/traefik diff --git a/services/traefik/.gitignore b/services/traefik/.gitignore deleted file mode 100644 index 08a7346..0000000 --- a/services/traefik/.gitignore +++ /dev/null @@ -1 +0,0 @@ -acme.json diff --git a/services/traefik/README.md b/services/traefik/README.md deleted file mode 100644 index 45b4b6b..0000000 --- a/services/traefik/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Traefik - -[Traefik](https://traefik.io/) is a proxy / API Gateway. - -## Setup - -`inv traefik.start` will start the service. - -When initializing for the first time, you should first `touch acme.json` and `chmod 600 acme.json` to ensure that the -certificate can be created and accessed by Traefik properly. - -## Adding services - -Adding services is done via Docker Compose labels. Any service with the `traefik.enable` label will be picked up by the -gateway. diff --git a/services/traefik/docker-compose.yml b/services/traefik/docker-compose.yml deleted file mode 100644 index c7ae1d2..0000000 --- a/services/traefik/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.7' - -services: - traefik: - restart: always - image: traefik:v2.9 - ports: - - "80:80" - - "443:443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./traefik.toml:/traefik.toml - - ./traefik_dynamic.toml:/traefik_dynamic.toml - - ${CONFIG_ROOT}/acme.json:/acme.json - -networks: - default: - name: web diff --git a/services/traefik/traefik.toml b/services/traefik/traefik.toml deleted file mode 100644 index 5d239b1..0000000 --- a/services/traefik/traefik.toml +++ /dev/null @@ -1,28 +0,0 @@ -[entryPoints] - [entryPoints.web] - address = ":80" - [entryPoints.web.http.redirections.entryPoint] - to = "websecure" - scheme = "https" - [entryPoints.websecure] - address = ":443" - -[log] - level = "DEBUG" - -[api] - dashboard = true - -[certificatesResolvers.lets-encrypt.acme] - email = "traefik@karnov.club" - storage = "acme.json" - [certificatesResolvers.lets-encrypt.acme.tlsChallenge] - -[providers.docker] - watch = true - useBindPortIP = true - network = "web" - exposedByDefault = false - -[providers.file] - filename = "traefik_dynamic.toml" diff --git a/services/traefik/traefik_dynamic.toml b/services/traefik/traefik_dynamic.toml deleted file mode 100644 index 633b88e..0000000 --- a/services/traefik/traefik_dynamic.toml +++ /dev/null @@ -1,54 +0,0 @@ -[http.routers] - [http.routers.api] - rule = "Host(`spadinaistan.karnov.club`)" - entrypoints = ["websecure"] - middlewares = ["auth-service"] - service = "api@internal" - [http.routers.api.tls] - certResolver = "lets-encrypt" - - [http.routers.deluge] - rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/deluge/`)" - service = "deluge" - middlewares = ["deluge-base-headers", "auth-service", "deluge-stripprefix"] - [http.routers.deluge.tls] - certResolver = "lets-encrypt" - - [http.routers.bitwarden] - rule = "Host(`spadinaistan.karnov.club`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))" - service = "bitwarden" - middlewares = ["bitwarden-stripprefix"] - [http.routers.bitwarden.tls] - certResolver = "lets-encrypt" - - [http.routers.auth-service] - rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/auth/`)" - service = "auth-service" - [http.routers.auth-service.tls] - certResolver = "lets-encrypt" - -[http.middlewares] - [http.middlewares.deluge-base-headers.headers.customRequestHeaders] - X-Deluge-Base = "/deluge/" - - [http.middlewares.deluge-stripprefix.stripprefix] - prefixes = ["/deluge"] - - [http.middlewares.bitwarden-stripprefix.stripprefix] - prefixes = ["/bitwarden"] - - [http.middlewares.auth-service.forwardauth] - address = "http://auth-service:8080/auth/login" - -[http.services] - [http.services.deluge.loadBalancer] - [[http.services.deluge.loadBalancer.servers]] - url = "http://deluge:8112/" - - [http.services.bitwarden.loadBalancer] - [[http.services.bitwarden.loadBalancer.servers]] - url = "http://bitwarden:8080/" - - [http.services.auth-service.loadBalancer] - [[http.services.auth-service.loadBalancer.servers]] - url = "http://auth-service:8080/" diff --git a/services/traefik/traefik_dynamic_local.toml b/services/traefik/traefik_dynamic_local.toml deleted file mode 100644 index 8f48d50..0000000 --- a/services/traefik/traefik_dynamic_local.toml +++ /dev/null @@ -1,53 +0,0 @@ -[http.routers] - [http.routers.api] - rule = "Host(`localhost`)" - entrypoints = ["web"] - middlewares = ["auth-service"] - service = "api@internal" - - [http.routers.deluge] - rule = "Host(`localhost`) && PathPrefix(`/deluge/`)" - service = "deluge" - middlewares = ["deluge-base-headers", "monolith-auth", "deluge-stripprefix"] - - [http.routers.monolith] - rule = "Host(`localhost`) && PathPrefix(`/app/`)" - service = "monolith" - - [http.routers.bitwarden] - rule = "Host(`localhost`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))" - service = "bitwarden" - middlewares = ["bitwarden-stripprefix"] - -[http.middlewares] - [http.middlewares.monolith-auth.forwardauth] - address = "http://monolith:8000/app/identity/me/" - - [http.middlewares.auth-service.forwardauth] - address = "http://auth-service:8080/auth/login" - - [http.middlewares.deluge-base-headers.headers.customRequestHeaders] - X-Deluge-Base = "/deluge/" - - [http.middlewares.deluge-stripprefix.stripprefix] - prefixes = ["/deluge"] - - [http.middlewares.bitwarden-stripprefix.stripprefix] - prefixes = ["/bitwarden"] - -[http.services] - [http.services.authservice.loadBalancer] - [[http.services.authservice.loadBalancer.servers]] - url = "http://authservice:8080/" - - [http.services.deluge.loadBalancer] - [[http.services.deluge.loadBalancer.servers]] - url = "http://deluge:8112/" - - [http.services.monolith.loadBalancer] - [[http.services.monolith.loadBalancer.servers]] - url = "http://monolith:8000/" - - [http.services.bitwarden.loadBalancer] - [[http.services.bitwarden.loadBalancer.servers]] - url = "http://bitwarden:8080/"