mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-06-16 11:58:52 -04:00
feat: add embedded authelia
This commit is contained in:
407
docs/adr/0005-embed-authelia-as-default-idp.md
Normal file
407
docs/adr/0005-embed-authelia-as-default-idp.md
Normal file
@@ -0,0 +1,407 @@
|
||||
---
|
||||
title: "Embed Authelia as the default Identity Provider"
|
||||
---
|
||||
|
||||
* Status: proposed
|
||||
* Deciders: [@micbar @rhafer @butonic @fschade]
|
||||
* Date: 2026-05-31
|
||||
|
||||
Reference: https://github.com/authelia/authelia/issues/5803, https://github.com/authelia/authelia/pull/8841
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
OpenCloud ships a fully self-contained, out-of-the-box authentication stack by
|
||||
embedding upstream Go libraries directly into its service binary:
|
||||
|
||||
- The **idm** service embeds `github.com/libregraph/idm` and runs an in-process
|
||||
LDAP server (`server.NewServer` in `services/idm/pkg/command/server.go`).
|
||||
- The **idp** service embeds `github.com/libregraph/lico` ("LibreConnect") as the
|
||||
OIDC provider. It boots lico via `bootstrap.Boot()`, extracts an
|
||||
`http.Handler` from the returned managers, and mounts it into OpenCloud's own
|
||||
chi router under a go-micro HTTP service
|
||||
(`services/idp/pkg/service/v0/service.go:118-127`).
|
||||
|
||||
Both dependencies are used **unforked, without `replace` directives**. This gives
|
||||
operators a working IdP with zero external dependencies — a key part of the
|
||||
OpenCloud value proposition.
|
||||
|
||||
lico is functional but minimal. It provides OIDC and a basic login form, but no
|
||||
second-factor authentication (TOTP / WebAuthn), no access-control policy engine,
|
||||
and no self-service SSO portal. The OpenCloud team has long been interested in
|
||||
**Authelia** as a richer embedded default IdP — it provides OIDC 1.0, 2FA
|
||||
(TOTP/WebAuthn/Duo), per-resource access control, a regulation/brute-force layer,
|
||||
and a complete login portal.
|
||||
|
||||
The blocker, raised by OpenCloud in [authelia/authelia#5803][issue] (2023), was
|
||||
that all of Authelia's logic lived in `internal/` and was therefore not
|
||||
importable from another Go module. [authelia/authelia#8841][pr] (merged
|
||||
2025-03-08) resolved this by adding a public, curated
|
||||
`github.com/authelia/authelia/v4/experimental/embed` package. As of `v4.39.20`
|
||||
(2026-05) this package is still present and actively maintained.
|
||||
|
||||
This ADR evaluates whether and how to embed Authelia, and what role it should
|
||||
play relative to the existing idp (lico) service.
|
||||
|
||||
[issue]: https://github.com/authelia/authelia/issues/5803
|
||||
[pr]: https://github.com/authelia/authelia/pull/8841
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
* **Out-of-the-box experience** — keep the "single binary, no external auth
|
||||
dependency" property that lico provides today.
|
||||
* **Feature parity and beyond** — gain 2FA, access policies and an SSO portal
|
||||
without operators bolting on a separate product.
|
||||
* **Integration cost** — reuse of OpenCloud's runtime supervisor, config system,
|
||||
routing and middleware; minimal custom glue.
|
||||
* **Upstream stability** — the embed API is explicitly experimental; we must be
|
||||
able to absorb breakage on Authelia version bumps.
|
||||
* **Dependency weight** — added impact on the monolith binary's module graph,
|
||||
build time and binary size.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Option A — Supervised standalone service (run the whole Authelia daemon)
|
||||
|
||||
This is the integration model the `experimental/embed` package is designed for.
|
||||
The public surface is intentionally small and config-file driven:
|
||||
|
||||
```go
|
||||
ctx, val, err := embed.New(paths []string, filterNames []string) // load + validate config
|
||||
err = embed.ProvidersStartupCheck(ctx, log)
|
||||
err = embed.ServiceRunAll(ctx) // run all Authelia services
|
||||
```
|
||||
|
||||
`embed.ServiceRunAll` → `service.RunAll` provisions the full set of Authelia
|
||||
services: the main `fasthttp` server **with its own listener**, the metrics
|
||||
server, the users file watcher, and the logging signal handler
|
||||
(`internal/service/provider.go` `GetProvisioners()`).
|
||||
|
||||
We would add an OpenCloud service (e.g. `services/auth-authelia` or a new mode of
|
||||
the existing idp service) whose `Server()` command:
|
||||
|
||||
1. Renders an Authelia YAML config from OpenCloud's config system to a file **on
|
||||
every start** (with secrets persisted separately; see "Config generation"
|
||||
below) — mirroring how the idp (lico) service generates its clients
|
||||
registration config in `NewService` (`createTemporaryClientsConfig`). It does
|
||||
not depend on `opencloud init`.
|
||||
2. Calls `embed.New(paths, filters)` and surfaces validation errors/warnings.
|
||||
3. Wraps `embed.ServiceRunAll(ctx)` in a `runner.Runner` registered with the
|
||||
suture supervisor in `opencloud/pkg/runtime/service/service.go`, exactly like
|
||||
other services.
|
||||
|
||||
Authelia owns its own port, router, lifecycle and config hot-reload.
|
||||
|
||||
#### Pros:
|
||||
|
||||
* Matches the embed package's intended usage → least friction with upstream, most
|
||||
likely to keep working across version bumps.
|
||||
* Reuses Authelia's own server, TLS, metrics and hot-reload — little glue code.
|
||||
* Full feature set (portal, 2FA, policies) available immediately.
|
||||
|
||||
#### Cons:
|
||||
|
||||
* Authelia binds its **own** listener; it is not mounted into OpenCloud's chi
|
||||
router. OpenCloud cannot inject its logging/tracing/static-asset middleware the
|
||||
way it does for lico. Tracing and access-log correlation need separate wiring.
|
||||
* Config is file/env driven, not struct driven (see "Config" note below) — we
|
||||
must generate and template Authelia config.
|
||||
* Two HTTP servers in the binary (Authelia's fasthttp + go-micro), each with its
|
||||
own port/proxy entry.
|
||||
|
||||
### Option B — Handler-mounted, lico-style
|
||||
|
||||
Mirror the current idp integration: obtain an `http.Handler` from Authelia and
|
||||
mount it into OpenCloud's existing chi router / go-micro HTTP service, reusing
|
||||
OpenCloud middleware.
|
||||
|
||||
The `experimental/embed/provider` subpackage exposes individual constructors
|
||||
(`provider.New(config, caCertPool)` for the full `middlewares.Providers`,
|
||||
`provider.NewOpenIDConnect`, `NewSession`, `NewAuthorizer`, …). In principle one
|
||||
could assemble providers and build the server handler manually.
|
||||
|
||||
#### Pros:
|
||||
|
||||
* Identical integration shape to lico → uniform routing, middleware, tracing,
|
||||
single HTTP server.
|
||||
|
||||
#### Cons:
|
||||
|
||||
* The embed package does **not** export the assembled `*fasthttp.RequestHandler`
|
||||
/ router. `server.New(...)` lives in `internal/` and returns a `*fasthttp.Server`,
|
||||
not a `net/http.Handler` — Authelia is fasthttp-based, lico is `net/http`-based,
|
||||
so there is no drop-in handler to mount into chi.
|
||||
* We would be re-implementing Authelia's `internal/server` wiring against an API
|
||||
the maintainers explicitly reserve the right to break ("methods may panic if
|
||||
not properly utilized", `doc.go`). High maintenance risk, fights upstream
|
||||
intent.
|
||||
* fasthttp ↔ net/http adaptation adds overhead and edge cases (hijacking,
|
||||
streaming, header semantics).
|
||||
|
||||
### Option C — Keep lico, do nothing
|
||||
|
||||
Stay on the current lico-based idp.
|
||||
|
||||
#### Pros:
|
||||
|
||||
* Zero work and zero new dependencies.
|
||||
|
||||
#### Cons:
|
||||
|
||||
* No 2FA, no policy engine, no portal — the gap that motivated this ADR remains.
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
**Option A (supervised standalone service). Authelia is now the default IdP in
|
||||
supervised (`opencloud server`) mode; the lico `idp` service is kept as an opt-in
|
||||
alternative.**
|
||||
|
||||
Rationale:
|
||||
|
||||
* Option A is the only path that aligns with how `experimental/embed` is
|
||||
designed and tested upstream, which is the single biggest factor in keeping a
|
||||
declared-experimental dependency maintainable.
|
||||
* Option B fights the fasthttp/`internal` boundary and would couple us to
|
||||
unexported wiring the maintainers refuse to stabilize.
|
||||
|
||||
The integration first shipped opt-in (lico default, Authelia behind a flag) to
|
||||
de-risk it. It is now promoted to the **default**, with lico demoted to an opt-in
|
||||
fallback so an exit path is preserved. See "Making Authelia the default" below for
|
||||
the concrete wiring.
|
||||
|
||||
Because Authelia maps to the **idp / OIDC-provider** role — not the LDAP **idm**
|
||||
role — it supplements/replaces lico. The embedded libregraph **idm** LDAP server
|
||||
stays as Authelia's `authentication_backend` (`provider.NewAuthenticationLDAP`),
|
||||
so the existing user directory is reused unchanged.
|
||||
|
||||
### Making Authelia the default
|
||||
|
||||
Three changes flip the default from lico to Authelia (all in supervised
|
||||
`opencloud server` mode):
|
||||
|
||||
* **Service run-set** (`opencloud/pkg/runtime/service/service.go`): `auth-authelia`
|
||||
moves to the core `reg(3, ...)` group (runs by default); `idp` moves to the
|
||||
optional `areg(...)` group (registered but off). Falling back to lico is then
|
||||
`OC_ADD_RUN_SERVICES=idp` + `OC_EXCLUDE_RUN_SERVICES=auth-authelia` (plus the
|
||||
issuer/route notes below).
|
||||
|
||||
* **OIDC issuer = `<OC_URL>/authelia`.** Nine services derive the issuer from
|
||||
`OC_URL;OC_OIDC_ISSUER;…` (last value wins, so `OC_OIDC_ISSUER` overrides
|
||||
`OC_URL`). Authelia serves OIDC under the `/authelia` base path, so the issuer
|
||||
is the subpath. The `server` command sets `OC_OIDC_ISSUER=<OC_URL>/authelia`
|
||||
before config parsing when it is unset, which propagates to all nine without
|
||||
per-service wiring. An explicit `OC_OIDC_ISSUER` (external IdP, or lico fallback
|
||||
which needs the root `<OC_URL>`) is left untouched. Standalone single-service
|
||||
deployments set `OC_OIDC_ISSUER` themselves.
|
||||
|
||||
*Why not serve Authelia at the root so `issuer = OC_URL` needs no change?*
|
||||
Authelia serves its login portal and its OIDC endpoints under one shared base
|
||||
path and derives the issuer from it (`IssuerURL()` → `BasePath()`); the portal
|
||||
index is `GET /`. At the root the portal would collide with the OpenCloud web UI
|
||||
(also `/`), breaking the redirect-based login. The subpath is required, hence
|
||||
the issuer subpath.
|
||||
|
||||
* **Proxy routes** (`services/proxy/.../defaultconfig.go`): the `/authelia` route
|
||||
(→ Authelia's listener) stays; the lico-specific `/konnect/` and `/signin/`
|
||||
routes and the root `/.well-known/openid-configuration` route are removed —
|
||||
discovery is now at `<OC_URL>/authelia/.well-known/openid-configuration`, served
|
||||
by the `/authelia` route. The default `proxy.oidc.issuer` literal becomes
|
||||
`https://localhost:9200/authelia` for the no-env localhost case.
|
||||
|
||||
* **Access-token verification = `none`** (`proxy.oidc.access_token_verify_method`):
|
||||
Authelia issues **opaque** access tokens, whereas lico issued JWTs. The proxy's
|
||||
`jwt` method verifies the token locally against the JWKS and cannot validate an
|
||||
opaque token, so the default is `none`: local JWT verification is skipped and the
|
||||
token is validated against Authelia's `/userinfo` endpoint instead (`SkipUserInfo`
|
||||
stays `false`; the result is cached). This is validation by the IdP, not a bypass.
|
||||
The proxy already anticipates this pairing (see the Authelia branch in
|
||||
`services/proxy/pkg/middleware/oidc_auth.go`). The cost is a cached `/userinfo`
|
||||
round-trip per new token; to avoid it, configure Authelia clients with
|
||||
`access_token_signed_response_alg: 'RS256'` (JWT access tokens) and switch the
|
||||
method back to `jwt`.
|
||||
|
||||
### Service startup ordering
|
||||
|
||||
`auth-authelia` runs in priority group 4 (with `frontend`), not group 3. Its
|
||||
provider startup check dials the idm LDAP server (`ldaps://…:9235`), which the idm
|
||||
service brings up in group 3; the runtime's post-group-3 wait ensures idm is
|
||||
listening before group 4 starts. Registering it in group 3 races idm and fails the
|
||||
startup check with "connection refused". (As an `areg`/optional service it was
|
||||
implicitly scheduled after all groups, which masked the ordering requirement.)
|
||||
|
||||
### Risks and mitigations
|
||||
|
||||
* **Experimental API instability.** `doc.go` warns of breaking changes at any
|
||||
minor bump and methods that may panic. *Mitigation:* pin exact Authelia
|
||||
versions, gate upgrades behind an integration test that boots
|
||||
`embed.New` + `ServiceRunAll` and exercises an OIDC flow, budget for glue
|
||||
churn per bump.
|
||||
* **Dependency graph / binary bloat.** Authelia pulls in fasthttp, its own SQL
|
||||
storage (postgres/mysql/sqlite), webauthn, totp, session and regulation.
|
||||
*Mitigation:* run the spike below first and measure `go mod graph` deltas,
|
||||
build time and binary size before committing.
|
||||
* **Config model mismatch.** `embed.Configuration` is a type **alias** to the
|
||||
`internal` `schema.Configuration`; external code can hold the value but cannot
|
||||
import the internal package to construct nested fields. *Mitigation:* drive
|
||||
configuration through a generated YAML file + `embed.New(paths, filters)` (the
|
||||
approach prototyped by @rhafer in 2023), not by building structs in Go. The
|
||||
service renders that file itself on every start (see "Config generation" below),
|
||||
so there is no struct-construction path to maintain.
|
||||
* **Two HTTP servers / proxy wiring.** *Mitigation:* register Authelia's port in
|
||||
the OpenCloud proxy/route table; document the topology for operators.
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. **Dependency spike** (timeboxed): in a scratch module, `go get
|
||||
github.com/authelia/authelia/v4@v4.39.20`, call `embed.New` with a minimal
|
||||
generated config and `embed.ServiceRunAll`, boot it standalone against the
|
||||
libregraph LDAP backend. Record module-graph, build-time and binary-size
|
||||
deltas. Go / no-go gate.
|
||||
2. **Re-engage upstream** ([#5803][issue] thread): confirm with @james-d-elliott
|
||||
that the supervised-daemon embed model covers our use case and clarify the
|
||||
support expectations for `experimental/embed`.
|
||||
3. **Config generation layer**: template an Authelia YAML config from OpenCloud's
|
||||
config system (issuer, OIDC clients, LDAP backend, session/storage, TOTP),
|
||||
rendered by the service itself on every start (secrets persisted separately)
|
||||
rather than by `opencloud init` (see "Config generation" below). This is the
|
||||
bulk of the work.
|
||||
4. **Service skeleton**: new opt-in service wrapping `embed.ServiceRunAll` in a
|
||||
`runner.Runner`, registered with the suture supervisor in
|
||||
`opencloud/pkg/runtime/service/service.go`; selectable vs. lico via config.
|
||||
5. **Routing / proxy**: register Authelia's listener in the proxy/route table;
|
||||
wire tracing and access-log correlation.
|
||||
6. **Tests & docs**: boot + OIDC-flow integration test pinned to the chosen
|
||||
Authelia version; admin docs in the docs repo (`/docs/admin/configuration/`)
|
||||
covering the lico ↔ Authelia choice and the new settings.
|
||||
7. **Review default**: once validated in production, revisit whether Authelia
|
||||
becomes the default IdP (a follow-up ADR referencing this one).
|
||||
|
||||
### Config generation
|
||||
|
||||
Authelia is configured entirely through a YAML file (`embed.New(paths)`), so the
|
||||
integration needs that file to exist with valid secrets, the OIDC clients, and
|
||||
the LDAP backend pointing at the embedded libregraph-idm directory. Two patterns
|
||||
were possible:
|
||||
|
||||
* **Render it from `opencloud init`** (the first prototype): init writes
|
||||
`authelia.yaml` next to `opencloud.yaml`. Rejected — it makes init aware of a
|
||||
second, foreign config schema, and no other service works this way.
|
||||
* **Render it from the service** (chosen): the `auth-authelia` service renders the
|
||||
Authelia config in its `Server` command, then calls `embed.New`. This mirrors
|
||||
the idp (lico) service, which generates its clients registration config in
|
||||
`NewService` (`createTemporaryClientsConfig`), and keeps init free of Authelia
|
||||
specifics. (See the Decision below for *when* it renders — on every start, with
|
||||
secrets persisted separately.)
|
||||
|
||||
Decision: **the service renders its own config on every start.** Concretely
|
||||
(`services/auth-authelia/pkg/render`).
|
||||
|
||||
A first prototype rendered `authelia.yaml` **once** (only if missing) and reused
|
||||
it thereafter, so the embedded secrets would persist. That turned out to be a
|
||||
trap: the file also froze every value *derived* from OpenCloud config (OIDC
|
||||
issuer / `OC_URL`, SMTP settings, the LDAP bind password). After an admin changed
|
||||
those in OpenCloud, the stale `authelia.yaml` silently kept the old values, with
|
||||
no signal that it no longer reflected the configuration.
|
||||
|
||||
The fix is to **separate the regenerated config from the persisted secrets**, the
|
||||
same split OpenCloud already uses elsewhere (e.g. lico's `createTemporaryClientsConfig`
|
||||
is rewritten on every start, while its signing keys are generate-once files).
|
||||
Authelia deep-merges multiple config files, so the service passes a small set:
|
||||
|
||||
* **`authelia.yaml` — regenerated on every start** from the current OpenCloud
|
||||
configuration, so it can never go stale. It holds only non-secret, derived
|
||||
values (issuer/domain, OIDC clients, LDAP backend incl. bind password, storage
|
||||
path, notifier, log level) and carries a `GENERATED FILE - DO NOT EDIT` header
|
||||
that points admins at OpenCloud config / env vars.
|
||||
* **`authelia.secrets.yaml` — generated once**, then left untouched. It holds the
|
||||
random secrets (session, storage-encryption, OIDC HMAC and RSA signing key,
|
||||
password-reset JWT). Persisting these keeps active sessions and issued OIDC
|
||||
tokens valid across restarts and config regenerations; deleting the file forces
|
||||
fresh secrets.
|
||||
* **`authelia.override.yaml` — optional, admin-managed**, never written by
|
||||
OpenCloud, merged **last** so it takes precedence. This restores the ability to
|
||||
customise Authelia (settings OpenCloud does not manage) without reintroducing
|
||||
the staleness problem, since the generated file is no longer the edit surface.
|
||||
|
||||
The public URL (OIDC issuer, session cookie domain, client redirect URIs) comes
|
||||
from the shared `OC_URL` (`cfg.Commons.OpenCloudURL`).
|
||||
|
||||
**Secrets vs. init's role.** The Authelia-internal secrets are generated and
|
||||
persisted by the service (in `authelia.secrets.yaml`), not by init. The only value
|
||||
shared with the rest of OpenCloud is the **LDAP bind password**: rather than
|
||||
seeding a dedicated `authelia` LDAP account, Authelia **reuses the existing `idp`
|
||||
service user** (`uid=idp,ou=sysusers,o=libregraph-idm`) — the same account the
|
||||
lico idp binds as. As with every LDAP service user, `opencloud init` generates
|
||||
that password and persists it in `opencloud.yaml`; for Authelia it is written to
|
||||
`auth_authelia.ldap.bind_password` (mirroring `idp.ldap.bind_password`, and equal
|
||||
to it) and the service reads it via its config. So init still owns *passwords*
|
||||
(consistent with all services) and seeds no new account, while the service owns
|
||||
the *Authelia config file*.
|
||||
|
||||
This keeps the "single binary, no external dependency" property: a fresh
|
||||
deployment that has run `opencloud init` and starts the opt-in `auth-authelia`
|
||||
service gets a working IdP with no extra config steps, and one whose generated
|
||||
config always reflects the live OpenCloud configuration.
|
||||
|
||||
### Serving the Authelia frontend
|
||||
|
||||
Authelia serves its **own** login portal — OpenCloud does not host the assets.
|
||||
The flow, verified end-to-end against a running `auth-authelia` service:
|
||||
|
||||
* The compiled React portal is **embedded into the binary** at compile time via
|
||||
`//go:embed public_html` in Authelia's `internal/server/asset.go`. There is no
|
||||
separate static-asset directory to deploy.
|
||||
* Authelia serves the portal, the OIDC endpoints and the assets from **its own
|
||||
HTTP listener** (`127.0.0.1:9091`, base path `/authelia`, set via
|
||||
`server.address` in the rendered `authelia.yaml`).
|
||||
* The OpenCloud proxy forwards `/authelia/*` to that listener (the
|
||||
`/authelia → http://127.0.0.1:9091` route in
|
||||
`services/proxy/pkg/config/defaults/defaultconfig.go`).
|
||||
* **Base path is automatic.** Authelia strips its configured router path
|
||||
(`middlewares.StripPath`) and templates `<base href=".../authelia/">` into
|
||||
`index.html`, so the SPA's relative `./static/...` asset URLs resolve under
|
||||
`/authelia` without any rewriting on the OpenCloud side.
|
||||
* **OIDC issuer needs forwarded headers.** Authelia derives the effective issuer
|
||||
from `X-Forwarded-Proto` / `X-Forwarded-Host`. The OpenCloud proxy sets these
|
||||
(`req.SetXForwarded()` in `services/proxy/pkg/router/router.go`), so behind the
|
||||
TLS-terminating proxy the issuer resolves to `https://<OC_URL>/authelia`.
|
||||
Hitting the `:9091` listener directly over plain HTTP returns `400` on the OIDC
|
||||
endpoints — this is expected, not a bug.
|
||||
|
||||
#### Building the embedded frontend
|
||||
|
||||
`go:embed` only captures what is on disk **when the OpenCloud binary is
|
||||
compiled**. While we build against a local `replace` checkout of Authelia (rather
|
||||
than a published module), the frontend must be built into that checkout first.
|
||||
Two sharp edges, both hit during the spike:
|
||||
|
||||
1. **`vite build` wipes the committed `api/` placeholders.** Authelia commits
|
||||
`internal/server/public_html/api/{index.html,openapi.yml}` as **0-byte
|
||||
placeholders** (the real Swagger docs are generated by a separate build step).
|
||||
Authelia's `templates.LoadTemplatedAssets` **requires both files to exist** at
|
||||
startup. `vite build` empties the output dir and deletes them, so the service
|
||||
then fails to boot with
|
||||
`loading template 'assets/public_html/api/index.html': file does not exist`.
|
||||
The empty placeholders are enough (they only blank the `/api` docs page; the
|
||||
portal and OIDC are unaffected).
|
||||
|
||||
2. **`go:embed` is compile-time.** Changing the assets on disk does nothing to an
|
||||
already-built binary; OpenCloud must be rebuilt afterwards.
|
||||
|
||||
The reproducible sequence is therefore:
|
||||
|
||||
```bash
|
||||
# 1. build the portal into the Authelia checkout (the replace target)
|
||||
cd <authelia-checkout>/web && pnpm install && pnpm build # vite → internal/server/public_html
|
||||
|
||||
# 2. restore the committed api/ placeholders that vite deleted
|
||||
git -C <authelia-checkout> checkout -- internal/server/public_html/api
|
||||
|
||||
# 3. rebuild the OpenCloud binary so go:embed captures the real assets
|
||||
cd <opencloud> && make build
|
||||
```
|
||||
|
||||
This friction is a direct consequence of the local `replace` directive and
|
||||
**disappears once we depend on a published `authelia/v4` release**: the module zip
|
||||
on the proxy ships `public_html` already built, including the generated `api/`
|
||||
docs. Until then, steps 1–3 (or an equivalent build target) must run before any
|
||||
build that needs a working portal.
|
||||
25
go.mod
25
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/opencloud-eu/opencloud
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
@@ -102,7 +102,7 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/image v0.40.0
|
||||
golang.org/x/net v0.55.0
|
||||
@@ -138,6 +138,7 @@ require (
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/authelia/authelia/v4 v4.39.20 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.22.0 // indirect
|
||||
@@ -188,7 +189,7 @@ require (
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -199,10 +200,10 @@ require (
|
||||
github.com/evanphx/json-patch/v5 v5.5.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/gdexlab/go-render v1.0.1 // indirect
|
||||
github.com/go-acme/lego/v4 v4.4.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20260416181348-e7dc79048676 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
@@ -223,7 +224,7 @@ require (
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-test/deep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
@@ -257,7 +258,7 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juliangruber/go-intersect v1.1.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
|
||||
@@ -279,7 +280,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.42 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
||||
github.com/maxymania/go-system v0.0.0-20170110133659-647cc364bf0b // indirect
|
||||
github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
@@ -328,7 +329,7 @@ require (
|
||||
github.com/prometheus/alertmanager v0.31.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.22.8 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
@@ -360,7 +361,7 @@ require (
|
||||
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
@@ -387,7 +388,7 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
@@ -415,3 +416,5 @@ replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/opencloud-
|
||||
|
||||
// to get the logger injection (https://github.com/pablodz/inotifywaitgo/pull/11)
|
||||
replace github.com/pablodz/inotifywaitgo v0.0.9 => github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9
|
||||
|
||||
replace github.com/authelia/authelia/v4 => /Users/m.barz/Code/opencloud/authelia
|
||||
|
||||
22
go.sum
22
go.sum
@@ -131,6 +131,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/authelia/authelia/v4 v4.39.20 h1:nM/fmvpLtlJ1Nx9SttGsVFQF6NRun85en8y/MiVJeAk=
|
||||
github.com/authelia/authelia/v4 v4.39.20/go.mod h1:i8DrZ4QBQH0vX/OYS2cvW7IMKCcf8+hLYpwzcH9PRT8=
|
||||
github.com/aws/aws-sdk-go v1.37.27/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/bbalet/stopwords v1.0.0 h1:0TnGycCtY0zZi4ltKoOGRFIlZHv0WqpoIGUsObjztfo=
|
||||
github.com/bbalet/stopwords v1.0.0/go.mod h1:sAWrQoDMfqARGIn4s6dp7OW7ISrshUD8IP2q3KoqPjc=
|
||||
@@ -303,6 +305,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/dnsimple/dnsimple-go v0.63.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
@@ -351,6 +355,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U=
|
||||
@@ -373,6 +379,8 @@ github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro
|
||||
github.com/go-asn1-ber/asn1-ber v1.4.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20260416181348-e7dc79048676 h1:/8x2r8Tu++vpNYlO8ACB3cdwoFYDGGxkOSOLLHN9E2o=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20260416181348-e7dc79048676/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
@@ -468,6 +476,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
@@ -721,6 +731,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -822,6 +834,8 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
|
||||
github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -1055,6 +1069,8 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
|
||||
github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0=
|
||||
github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM=
|
||||
@@ -1213,6 +1229,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
@@ -1341,6 +1359,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1364,6 +1384,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/opencloud/pkg/register"
|
||||
"github.com/opencloud-eu/opencloud/opencloud/pkg/runtime"
|
||||
"github.com/opencloud-eu/opencloud/pkg/config"
|
||||
@@ -16,6 +19,7 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
Use: "server",
|
||||
Short: "start a fullstack server (runtime and all services in supervised mode)",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
defaultOIDCIssuerToAuthelia()
|
||||
return configlog.ReturnError(parser.ParseConfig(cfg, false))
|
||||
},
|
||||
GroupID: CommandGroupServer,
|
||||
@@ -27,6 +31,26 @@ func Server(cfg *config.Config) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// defaultOIDCIssuerToAuthelia points the shared OIDC issuer at the embedded Authelia provider, which
|
||||
// is the default IdP in supervised mode. Authelia serves OIDC under the '/authelia' base path, so the
|
||||
// issuer is '<OC_URL>/authelia'. Every issuer-consuming service honors OC_OIDC_ISSUER, so setting it
|
||||
// here (before config parsing) propagates the value to all of them without per-service wiring.
|
||||
//
|
||||
// It only sets a default: an explicit OC_OIDC_ISSUER is left untouched (e.g. for an external IdP, or
|
||||
// when falling back to the lico 'idp' service, which serves OIDC at the root and needs OC_URL).
|
||||
func defaultOIDCIssuerToAuthelia() {
|
||||
if _, ok := os.LookupEnv("OC_OIDC_ISSUER"); ok {
|
||||
return
|
||||
}
|
||||
|
||||
ocURL := os.Getenv("OC_URL")
|
||||
if ocURL == "" {
|
||||
ocURL = "https://localhost:9200"
|
||||
}
|
||||
|
||||
_ = os.Setenv("OC_OIDC_ISSUER", strings.TrimRight(ocURL, "/")+"/authelia")
|
||||
}
|
||||
|
||||
func init() {
|
||||
register.AddCommand(Server)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
appregistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command"
|
||||
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command"
|
||||
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command"
|
||||
authauthelia "github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/command"
|
||||
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command"
|
||||
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/command"
|
||||
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command"
|
||||
@@ -83,6 +84,11 @@ var serviceCommands = []register.Command{
|
||||
cfg.AuthApp.Commons = cfg.Commons
|
||||
})
|
||||
},
|
||||
func(cfg *config.Config) *cobra.Command {
|
||||
return ServiceCommand(cfg, cfg.AuthAuthelia.Service.Name, authauthelia.GetCommands(cfg.AuthAuthelia), func(c *config.Config) {
|
||||
cfg.AuthAuthelia.Commons = cfg.Commons
|
||||
})
|
||||
},
|
||||
func(cfg *config.Config) *cobra.Command {
|
||||
return ServiceCommand(cfg, cfg.AuthBasic.Service.Name, authbasic.GetCommands(cfg.AuthBasic), func(c *config.Config) {
|
||||
cfg.AuthBasic.Commons = cfg.Commons
|
||||
|
||||
@@ -200,6 +200,15 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword
|
||||
},
|
||||
},
|
||||
},
|
||||
// The auth-authelia service renders its own authelia.yaml and binds to the embedded LDAP
|
||||
// directory as the idm admin user (uid=libregraph) so it can modify user passwords (password
|
||||
// change). That account's password is the 'idm' service-user password, reused here rather than
|
||||
// seeding a dedicated Authelia account.
|
||||
AuthAuthelia: LdapBasedService{
|
||||
Ldap: LdapSettings{
|
||||
BindPassword: idmServicePassword,
|
||||
},
|
||||
},
|
||||
Collaboration: Collaboration{
|
||||
WopiApp: WopiApp{
|
||||
Secret: collaborationWOPISecret,
|
||||
|
||||
@@ -29,6 +29,7 @@ type OpenCloudConfig struct {
|
||||
Proxy ProxyService `yaml:"proxy"`
|
||||
Frontend FrontendService `yaml:"frontend"`
|
||||
AuthBasic AuthbasicService `yaml:"auth_basic"`
|
||||
AuthAuthelia LdapBasedService `yaml:"auth_authelia"`
|
||||
AuthBearer AuthbearerService `yaml:"auth_bearer"`
|
||||
Users UsersAndGroupsService `yaml:"users"`
|
||||
Groups UsersAndGroupsService `yaml:"groups"`
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command"
|
||||
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command"
|
||||
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command"
|
||||
authauthelia "github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/command"
|
||||
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command"
|
||||
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command"
|
||||
authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/command"
|
||||
@@ -273,11 +274,6 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) {
|
||||
cfg.Webfinger.Commons = cfg.Commons
|
||||
return webfinger.Execute(cfg.Webfinger)
|
||||
})
|
||||
reg(3, opts.Config.IDP.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
|
||||
cfg.IDP.Context = ctx
|
||||
cfg.IDP.Commons = cfg.Commons
|
||||
return idp.Execute(cfg.IDP)
|
||||
})
|
||||
reg(3, opts.Config.Proxy.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
|
||||
cfg.Proxy.Context = ctx
|
||||
cfg.Proxy.Commons = cfg.Commons
|
||||
@@ -308,10 +304,28 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) {
|
||||
return frontend.Execute(cfg.Frontend)
|
||||
})
|
||||
|
||||
// auth-authelia is the default identity provider (OIDC). It runs in priority group 4 (after the
|
||||
// post-group-3 wait) because its startup check dials the idm LDAP server (ldaps://...:9235), which
|
||||
// is brought up by the idm service in group 3 - starting it in group 3 would race idm and fail with
|
||||
// "connection refused". The lico-based 'idp' service is kept as an opt-in alternative (registered
|
||||
// as an optional service below).
|
||||
reg(4, opts.Config.AuthAuthelia.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
|
||||
cfg.AuthAuthelia.Context = ctx
|
||||
cfg.AuthAuthelia.Commons = cfg.Commons
|
||||
return authauthelia.Execute(cfg.AuthAuthelia)
|
||||
})
|
||||
|
||||
// populate optional services
|
||||
areg := func(name string, exec func(context.Context, *occfg.Config) error) {
|
||||
s.Additional[name] = NewSutureServiceBuilder(name, exec)
|
||||
}
|
||||
// The lico-based 'idp' service is the opt-in alternative to the default auth-authelia provider.
|
||||
// Enable it via OC_ADD_RUN_SERVICES (and disable auth-authelia) to fall back to lico.
|
||||
areg(opts.Config.IDP.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
|
||||
cfg.IDP.Context = ctx
|
||||
cfg.IDP.Commons = cfg.Commons
|
||||
return idp.Execute(cfg.IDP)
|
||||
})
|
||||
areg(opts.Config.Antivirus.Service.Name, func(ctx context.Context, cfg *occfg.Config) error {
|
||||
cfg.Antivirus.Context = ctx
|
||||
cfg.Antivirus.Commons = cfg.Commons
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config"
|
||||
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config"
|
||||
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config"
|
||||
authAuthelia "github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config"
|
||||
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config"
|
||||
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config"
|
||||
@@ -88,6 +89,7 @@ type Config struct {
|
||||
AppRegistry *appRegistry.Config `yaml:"app_registry"`
|
||||
Audit *audit.Config `yaml:"audit"`
|
||||
AuthApp *authapp.Config `yaml:"auth_app"`
|
||||
AuthAuthelia *authAuthelia.Config `yaml:"auth_authelia"`
|
||||
AuthBasic *authbasic.Config `yaml:"auth_basic"`
|
||||
AuthBearer *authbearer.Config `yaml:"auth_bearer"`
|
||||
AuthMachine *authmachine.Config `yaml:"auth_machine"`
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config/defaults"
|
||||
audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config/defaults"
|
||||
authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config/defaults"
|
||||
authAuthelia "github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config/defaults"
|
||||
authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config/defaults"
|
||||
authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config/defaults"
|
||||
authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config/defaults"
|
||||
@@ -63,6 +64,7 @@ func DefaultConfig() *Config {
|
||||
AppRegistry: appRegistry.DefaultConfig(),
|
||||
Audit: audit.DefaultConfig(),
|
||||
AuthApp: authapp.DefaultConfig(),
|
||||
AuthAuthelia: authAuthelia.DefaultConfig(),
|
||||
AuthBasic: authbasic.DefaultConfig(),
|
||||
AuthBearer: authbearer.DefaultConfig(),
|
||||
AuthMachine: authmachine.DefaultConfig(),
|
||||
|
||||
30
services/auth-authelia/pkg/command/health.go
Normal file
30
services/auth-authelia/pkg/command/health.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
//
|
||||
// Authelia exposes its own health endpoint on the listener it manages itself (see the rendered
|
||||
// authelia.yaml). The OpenCloud service therefore only validates that its configuration can be
|
||||
// parsed and that the Authelia configuration file is referenced.
|
||||
func Health(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "check health status",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnError(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
logger.Debug().Str("config", cfg.ConfigPath).Msg("Health configuration is valid")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
35
services/auth-authelia/pkg/command/root.go
Normal file
35
services/auth-authelia/pkg/command/root.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/clihelper"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) []*cobra.Command {
|
||||
return []*cobra.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the opencloud auth-authelia command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cobra.Command{
|
||||
Use: "auth-authelia",
|
||||
Short: "Serve the embedded Authelia authentication provider for OpenCloud",
|
||||
})
|
||||
|
||||
app.AddCommand(GetCommands(cfg)...)
|
||||
app.SetArgs(os.Args[1:])
|
||||
|
||||
return app.ExecuteContext(cfg.Context)
|
||||
}
|
||||
98
services/auth-authelia/pkg/command/server.go
Normal file
98
services/auth-authelia/pkg/command/server.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/signal"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/configlog"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/runner"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config/parser"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/render"
|
||||
|
||||
embed "github.com/authelia/authelia/v4/experimental/embed"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "server",
|
||||
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if cfg.Context == nil {
|
||||
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
|
||||
defer cancel()
|
||||
}
|
||||
ctx := cfg.Context
|
||||
|
||||
// Render the Authelia configuration. The main config file is regenerated from the current
|
||||
// OpenCloud configuration on every start (so it never goes stale), while the secrets file is
|
||||
// generated once and reused. This mirrors how the idp service generates its config in
|
||||
// NewService, and is why the service does not depend on 'opencloud init' to produce the file.
|
||||
configPaths, err := render.Config(logger, cfg)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Str("config", cfg.ConfigPath).Msg("failed to render authelia configuration")
|
||||
return err
|
||||
}
|
||||
|
||||
// The embedded Authelia services watch this context for cancellation. When the OpenCloud
|
||||
// supervisor (or a process signal) cancels it, Authelia performs a graceful shutdown.
|
||||
runCtx, runCancel := context.WithCancel(ctx)
|
||||
defer runCancel()
|
||||
|
||||
// Build the embedded Authelia context from the rendered configuration files (Authelia
|
||||
// deep-merges them). This validates the configuration and instantiates all Authelia
|
||||
// providers (LDAP backend, storage, OIDC, ...).
|
||||
actx, validator, err := embed.NewWithContext(runCtx, configPaths, cfg.FilterNames)
|
||||
if err != nil {
|
||||
if validator != nil {
|
||||
for _, verr := range validator.Errors() {
|
||||
logger.Error().Err(verr).Strs("config", configPaths).Msg("invalid authelia configuration")
|
||||
}
|
||||
}
|
||||
logger.Error().Err(err).Strs("config", configPaths).Msg("failed to initialize embedded authelia")
|
||||
return err
|
||||
}
|
||||
|
||||
// Run Authelia's provider startup checks before serving. This is what migrates the storage
|
||||
// schema to the latest version (creating the authentication_logs, banned_ip, ... tables);
|
||||
// without it the SQLite database stays empty and every auth attempt fails with "no such
|
||||
// table". The notifier (SMTP) and NTP startup checks are disabled in the rendered config so
|
||||
// an unreachable mail server or missing outbound NTP does not block startup.
|
||||
if err := embed.ProvidersStartupCheck(actx, true); err != nil {
|
||||
logger.Error().Err(err).Str("config", cfg.ConfigPath).Msg("authelia provider startup checks failed")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info().Str("config", cfg.ConfigPath).Msg("starting embedded authelia")
|
||||
|
||||
gr := runner.NewGroup()
|
||||
gr.Add(runner.New(cfg.Service.Name, func() error {
|
||||
// ServiceRunAll blocks until runCtx is cancelled (or Authelia receives a process signal),
|
||||
// running Authelia's own HTTP server, metrics server and watchers.
|
||||
return embed.ServiceRunAll(actx)
|
||||
}, func() {
|
||||
runCancel()
|
||||
}))
|
||||
|
||||
grResults := gr.Run(ctx)
|
||||
|
||||
// return the first non-nil error found in the results
|
||||
for _, grResult := range grResults {
|
||||
if grResult.RunnerError != nil {
|
||||
return grResult.RunnerError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
26
services/auth-authelia/pkg/command/version.go
Normal file
26
services/auth-authelia/pkg/command/version.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/version"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version prints the version of this binary.
|
||||
//
|
||||
// The embedded Authelia service does not register with the go-micro service registry (it serves its
|
||||
// own HTTP listener), so this command only prints the OpenCloud build version.
|
||||
func Version(_ *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "print the version of this binary",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Version: " + version.GetString())
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
64
services/auth-authelia/pkg/config/config.go
Normal file
64
services/auth-authelia/pkg/config/config.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts for the auth-authelia service.
|
||||
//
|
||||
// The auth-authelia service embeds Authelia (https://www.authelia.com) as an out-of-the-box
|
||||
// authentication provider and OpenID Connect provider. Unlike most other OpenCloud services it
|
||||
// does not expose a go-micro HTTP server. Instead it boots Authelia via the upstream
|
||||
// experimental/embed package, which serves its own HTTP listener. The OpenCloud proxy forwards
|
||||
// the relevant request paths to that listener.
|
||||
//
|
||||
// The actual Authelia configuration (LDAP backend, OIDC clients, storage, notifier, secrets) lives
|
||||
// in a dedicated YAML file. Mirroring how the idp (lico) service generates its clients registration
|
||||
// config, the auth-authelia service renders that file itself on first start (see pkg/render); it is
|
||||
// not produced by `opencloud init`. This service only needs to know where the file lives and the
|
||||
// shared 'authelia' LDAP bind password.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;AUTH_AUTHELIA_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"7.0.0"`
|
||||
|
||||
ConfigPath string `yaml:"config_path" env:"AUTH_AUTHELIA_CONFIG_PATH" desc:"Path to the Authelia configuration file. The auth-authelia service renders it on first start if it does not exist." introductionVersion:"7.0.0"`
|
||||
FilterNames []string `yaml:"-"` // Authelia config file filters (e.g. 'template'); empty by default.
|
||||
|
||||
LDAP LDAP `yaml:"ldap"`
|
||||
SMTP SMTP `yaml:"smtp"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// SMTP configures the Authelia mail notifier (password reset, 2FA registration). It is optional:
|
||||
// when Host is empty the service renders a filesystem notifier instead, so a fresh deployment works
|
||||
// without any mail server. The env vars default to the OpenCloud notifications service's SMTP
|
||||
// settings (so SMTP is configured once for the whole deployment), with AUTH_AUTHELIA_SMTP_* taking
|
||||
// precedence when an Authelia-specific mail server is wanted.
|
||||
type SMTP struct {
|
||||
Host string `yaml:"smtp_host" env:"NOTIFICATIONS_SMTP_HOST;AUTH_AUTHELIA_SMTP_HOST" desc:"SMTP host to connect to. When set, Authelia sends notification mails via SMTP; when empty it writes them to a file." introductionVersion:"7.0.0"`
|
||||
Port int `yaml:"smtp_port" env:"NOTIFICATIONS_SMTP_PORT;AUTH_AUTHELIA_SMTP_PORT" desc:"Port of the SMTP host to connect to." introductionVersion:"7.0.0"`
|
||||
Sender string `yaml:"smtp_sender" env:"NOTIFICATIONS_SMTP_SENDER;AUTH_AUTHELIA_SMTP_SENDER" desc:"Sender address of emails sent by Authelia (e.g. 'OpenCloud <noreply@example.com>')." introductionVersion:"7.0.0"`
|
||||
Username string `yaml:"smtp_username" env:"NOTIFICATIONS_SMTP_USERNAME;AUTH_AUTHELIA_SMTP_USERNAME" desc:"Username for SMTP authentication." introductionVersion:"7.0.0"`
|
||||
Password string `yaml:"smtp_password" env:"NOTIFICATIONS_SMTP_PASSWORD;AUTH_AUTHELIA_SMTP_PASSWORD" desc:"Password for SMTP authentication." introductionVersion:"7.0.0"`
|
||||
Encryption string `yaml:"smtp_encryption" env:"NOTIFICATIONS_SMTP_ENCRYPTION;AUTH_AUTHELIA_SMTP_ENCRYPTION" desc:"Encryption method for SMTP. Possible values are 'starttls', 'ssltls' and 'none'." introductionVersion:"7.0.0"`
|
||||
}
|
||||
|
||||
// LDAP holds the settings the service needs to render the Authelia LDAP authentication backend.
|
||||
// Authelia binds to the embedded libregraph-idm directory as the idm admin user (uid=libregraph) -
|
||||
// the only account the directory lets modify entries, which Authelia needs for password changes.
|
||||
// Only the bind password is configurable here; the bind DN and address are fixed to the embedded
|
||||
// directory.
|
||||
type LDAP struct {
|
||||
BindPassword string `yaml:"bind_password" env:"OC_LDAP_BIND_PASSWORD;AUTH_AUTHELIA_LDAP_BIND_PASSWORD" desc:"Password for the 'idm' admin LDAP user (uid=libregraph) Authelia binds as. Generated by 'opencloud init' (shared with the idm service)." introductionVersion:"7.0.0"`
|
||||
}
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
38
services/auth-authelia/pkg/config/defaults/defaultconfig.go
Normal file
38
services/auth-authelia/pkg/config/defaults/defaultconfig.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
)
|
||||
|
||||
// FullDefaultConfig returns a fully initialized default configuration.
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DefaultConfig returns a basic default configuration.
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Service: config.Service{
|
||||
Name: "auth-authelia",
|
||||
},
|
||||
// The config file is rendered by `opencloud init` into the OpenCloud config directory.
|
||||
ConfigPath: filepath.Join(defaults.BaseConfigPath(), "authelia.yaml"),
|
||||
FilterNames: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureDefaults adds default values to the configuration if they are not set yet.
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
if cfg.LogLevel == "" {
|
||||
cfg.LogLevel = "error"
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitizes the configuration.
|
||||
func Sanitize(_ *config.Config) {}
|
||||
42
services/auth-authelia/pkg/config/parser/parse.go
Normal file
42
services/auth-authelia/pkg/config/parser/parse.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
occfg "github.com/opencloud-eu/opencloud/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/envdecode"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config/defaults"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
// Validate validates the configuration.
|
||||
func Validate(cfg *config.Config) error {
|
||||
if cfg.ConfigPath == "" {
|
||||
return fmt.Errorf("the authelia config path is not configured for the %s service", cfg.Service.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
535
services/auth-authelia/pkg/render/render.go
Normal file
535
services/auth-authelia/pkg/render/render.go
Normal file
@@ -0,0 +1,535 @@
|
||||
// Package render generates the Authelia configuration consumed by the embedded Authelia provider.
|
||||
//
|
||||
// It writes two files (mirroring how the idp (lico) service generates its config in
|
||||
// services/idp/pkg/service/v0/service.go, and how OpenCloud keeps secrets separate from config):
|
||||
//
|
||||
// - authelia.yaml: regenerated from the current OpenCloud configuration on every start, so it
|
||||
// always reflects the live values (OC_URL, SMTP settings, the idp bind password, ...). It carries
|
||||
// a "do not edit" header because manual changes are overwritten.
|
||||
// - authelia.secrets.yaml: the random secrets (session, storage encryption, OIDC HMAC and signing
|
||||
// key, password-reset JWT). Generated once and then left untouched so existing sessions and OIDC
|
||||
// tokens survive restarts and config changes.
|
||||
//
|
||||
// Authelia deep-merges multiple config files, so the two are passed together. An optional
|
||||
// authelia.override.yaml (admin-managed, never generated) is merged last and takes precedence.
|
||||
//
|
||||
// There is intentionally no dependency on 'opencloud init': the only value the service needs from
|
||||
// init is the LDAP bind password of the libregraph-idm admin user it binds as, read from its config.
|
||||
package render
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/config/defaults"
|
||||
"github.com/opencloud-eu/opencloud/pkg/generators"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/services/auth-authelia/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
// secretLength is the length of the random secrets generated for Authelia (session, storage
|
||||
// encryption, OIDC HMAC and the password-reset JWT). Authelia recommends long secrets.
|
||||
secretLength = 64
|
||||
// oidcKeyBits is the size of the RSA key used by Authelia to sign OIDC tokens.
|
||||
oidcKeyBits = 2048
|
||||
// port is the loopback port the embedded Authelia HTTP server binds to. It must match the
|
||||
// backend referenced by the proxy route (services/proxy .../defaultconfig.go).
|
||||
port = 9091
|
||||
// basePath is the URL base path Authelia is served under. Serving Authelia on a subpath lets a
|
||||
// single proxy route forward to it without colliding with the web frontend or the lico idp.
|
||||
basePath = "authelia"
|
||||
// ldapBindDN is the DN of the LDAP service user Authelia binds as (service-user binding). It must
|
||||
// be the libregraph-idm admin DN, because that directory only permits its configured admin DN to
|
||||
// modify entries (e.g. a user's userPassword during a password change); a read-only account such
|
||||
// as 'uid=idp' is rejected with "Insufficient Access Rights". This is the same admin account the
|
||||
// graph service uses for user management. The matching password (the 'idm' service-user password)
|
||||
// is provided via cfg.LDAP.BindPassword.
|
||||
ldapBindDN = "uid=libregraph,ou=sysusers,o=libregraph-idm"
|
||||
// ldapAddress is the loopback LDAPS address of the embedded libregraph-idm server.
|
||||
ldapAddress = "ldaps://127.0.0.1:9235"
|
||||
|
||||
// secretsFilename is the generate-once secrets file, kept next to the main config file.
|
||||
secretsFilename = "authelia.secrets.yaml"
|
||||
// overrideFilename is an optional admin-managed file merged last (highest precedence). It is
|
||||
// never generated or modified by OpenCloud.
|
||||
overrideFilename = "authelia.override.yaml"
|
||||
)
|
||||
|
||||
// secretsData holds the values rendered into authelia.secrets.yaml.
|
||||
type secretsData struct {
|
||||
SessionSecret string
|
||||
StorageEncryptionKey string
|
||||
ResetPasswordJWTSecret string
|
||||
OIDCHMACSecret string
|
||||
OIDCKeyPEMIndented string
|
||||
}
|
||||
|
||||
// configData holds the (non-secret) values rendered into authelia.yaml on every start.
|
||||
type configData struct {
|
||||
LogLevel string
|
||||
Address string
|
||||
Domain string
|
||||
AutheliaURL string
|
||||
LDAPAddress string
|
||||
LDAPBindDN string
|
||||
LDAPPassword string
|
||||
StoragePath string
|
||||
OIDCClientsYAML string
|
||||
NotifierYAML string
|
||||
}
|
||||
|
||||
// Config writes the Authelia configuration files and returns the ordered list of paths to pass to
|
||||
// the embedded provider. The secrets file is generated once and reused; the main config file is
|
||||
// regenerated from the current OpenCloud configuration on every call so it never goes stale. An
|
||||
// optional authelia.override.yaml is appended last so admin overrides take precedence.
|
||||
//
|
||||
// The LDAP bind password is read from cfg.LDAP.BindPassword; Authelia binds as the libregraph-idm
|
||||
// admin user (uid=libregraph) so it can both look users up and modify passwords. No dedicated
|
||||
// Authelia LDAP account is needed.
|
||||
func Config(logger log.Logger, cfg *config.Config) ([]string, error) {
|
||||
targetPath := cfg.ConfigPath
|
||||
if targetPath == "" {
|
||||
return nil, fmt.Errorf("authelia config path is empty")
|
||||
}
|
||||
if cfg.LDAP.BindPassword == "" {
|
||||
return nil, fmt.Errorf("the 'authelia' LDAP bind password is not configured; run 'opencloud init' " +
|
||||
"so the password gets generated and seeded, or set AUTH_AUTHELIA_LDAP_BIND_PASSWORD")
|
||||
}
|
||||
|
||||
dir := filepath.Dir(targetPath)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("could not create authelia config directory: %w", err)
|
||||
}
|
||||
|
||||
storagePath := filepath.Join(defaults.BaseDataPath(), "authelia", "authelia.sqlite3")
|
||||
if err := os.MkdirAll(filepath.Dir(storagePath), 0700); err != nil {
|
||||
return nil, fmt.Errorf("could not create authelia storage directory: %w", err)
|
||||
}
|
||||
|
||||
// 1. Secrets: generate once, then leave untouched.
|
||||
secretsPath := filepath.Join(dir, secretsFilename)
|
||||
if err := ensureSecrets(logger, secretsPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Main config: always regenerated from the current OpenCloud configuration.
|
||||
ocURL := openCloudURL(cfg)
|
||||
domain := "localhost"
|
||||
if u, perr := url.Parse(ocURL); perr == nil && u.Hostname() != "" {
|
||||
domain = u.Hostname()
|
||||
}
|
||||
|
||||
data := configData{
|
||||
LogLevel: cfg.LogLevel,
|
||||
Address: fmt.Sprintf("tcp://127.0.0.1:%d/%s", port, basePath),
|
||||
Domain: domain,
|
||||
AutheliaURL: ocURL + "/" + basePath,
|
||||
LDAPAddress: ldapAddress,
|
||||
LDAPBindDN: ldapBindDN,
|
||||
LDAPPassword: cfg.LDAP.BindPassword,
|
||||
StoragePath: storagePath,
|
||||
OIDCClientsYAML: renderOIDCClients(ocURL),
|
||||
NotifierYAML: renderNotifier(cfg, domain),
|
||||
}
|
||||
if err := renderToFile(configTemplate, data, targetPath); err != nil {
|
||||
return nil, fmt.Errorf("could not write authelia config: %w", err)
|
||||
}
|
||||
logger.Info().Str("config", targetPath).Str("oidc_issuer", data.AutheliaURL).
|
||||
Msg("regenerated authelia config from the current OpenCloud configuration")
|
||||
|
||||
// Authelia requires the session cookie domain to contain a dot or be an IP address; it rejects
|
||||
// a bare hostname such as 'localhost' and refuses to start. The default OC_URL is
|
||||
// https://localhost:9200, so warn explicitly that Authelia needs a real domain.
|
||||
if !isValidCookieDomain(domain) {
|
||||
logger.Warn().Str("domain", domain).Str("config", targetPath).
|
||||
Msg("session cookie domain is not valid for Authelia (needs a dot or must be an IP); " +
|
||||
"Authelia will not start until 'session.cookies' uses a fully-qualified domain. Set " +
|
||||
"OC_URL to your public URL.")
|
||||
}
|
||||
|
||||
paths := []string{secretsPath, targetPath}
|
||||
|
||||
// 3. Optional admin override, merged last so it takes precedence.
|
||||
overridePath := filepath.Join(dir, overrideFilename)
|
||||
if _, err := os.Stat(overridePath); err == nil {
|
||||
logger.Info().Str("override", overridePath).Msg("merging authelia override config")
|
||||
paths = append(paths, overridePath)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("could not stat authelia override config %q: %w", overridePath, err)
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// ensureSecrets generates the secrets file if it does not exist yet. An existing file is left
|
||||
// untouched so the secrets persist across restarts and config regenerations.
|
||||
func ensureSecrets(logger log.Logger, path string) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
logger.Info().Str("secrets", path).Msg("using existing authelia secrets")
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("could not stat authelia secrets %q: %w", path, err)
|
||||
}
|
||||
|
||||
sessionSecret, err := generators.GenerateRandomPassword(secretLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate authelia session secret: %w", err)
|
||||
}
|
||||
storageKey, err := generators.GenerateRandomPassword(secretLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate authelia storage encryption key: %w", err)
|
||||
}
|
||||
resetJWTSecret, err := generators.GenerateRandomPassword(secretLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate authelia password reset jwt secret: %w", err)
|
||||
}
|
||||
oidcHMAC, err := generators.GenerateRandomPassword(secretLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate authelia oidc hmac secret: %w", err)
|
||||
}
|
||||
oidcKeyPEM, err := generateRSAPrivateKeyPEM(oidcKeyBits)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate authelia oidc signing key: %w", err)
|
||||
}
|
||||
|
||||
data := secretsData{
|
||||
SessionSecret: sessionSecret,
|
||||
StorageEncryptionKey: storageKey,
|
||||
ResetPasswordJWTSecret: resetJWTSecret,
|
||||
OIDCHMACSecret: oidcHMAC,
|
||||
OIDCKeyPEMIndented: indentLines(oidcKeyPEM, 10),
|
||||
}
|
||||
if err := renderToFile(secretsTemplate, data, path); err != nil {
|
||||
return fmt.Errorf("could not write authelia secrets: %w", err)
|
||||
}
|
||||
logger.Info().Str("secrets", path).Msg("generated authelia secrets")
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderToFile parses and executes the given template into path with mode 0600.
|
||||
func renderToFile(tmplText string, data any, path string) error {
|
||||
tmpl, err := template.New("authelia").Parse(tmplText)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse template: %w", err)
|
||||
}
|
||||
var buf strings.Builder
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("could not render template: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, []byte(buf.String()), 0600)
|
||||
}
|
||||
|
||||
// openCloudURL resolves the public OpenCloud URL used to derive the OIDC issuer, session domain and
|
||||
// the OIDC client redirect URIs. It prefers the shared commons value (env OC_URL), falling back to
|
||||
// OC_URL directly and finally the localhost default.
|
||||
func openCloudURL(cfg *config.Config) string {
|
||||
ocURL := ""
|
||||
if cfg.Commons != nil {
|
||||
ocURL = cfg.Commons.OpenCloudURL
|
||||
}
|
||||
if ocURL == "" {
|
||||
ocURL = os.Getenv("OC_URL")
|
||||
}
|
||||
if ocURL == "" {
|
||||
ocURL = "https://localhost:9200"
|
||||
}
|
||||
return strings.TrimRight(ocURL, "/")
|
||||
}
|
||||
|
||||
// isValidCookieDomain reports whether domain is acceptable as an Authelia session cookie domain.
|
||||
// Authelia requires either an IP address or a hostname containing at least one dot; a bare label
|
||||
// such as 'localhost' is rejected.
|
||||
func isValidCookieDomain(domain string) bool {
|
||||
if net.ParseIP(domain) != nil {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(domain, ".")
|
||||
}
|
||||
|
||||
// generateRSAPrivateKeyPEM generates a new RSA private key and returns it PEM (PKCS#1) encoded.
|
||||
func generateRSAPrivateKeyPEM(bits int) (string, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
return string(pem.EncodeToMemory(block)), nil
|
||||
}
|
||||
|
||||
// indentLines prefixes every non-empty line with n spaces. Used to embed multi-line PEM data into a
|
||||
// YAML block scalar.
|
||||
func indentLines(s string, n int) string {
|
||||
prefix := strings.Repeat(" ", n)
|
||||
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
||||
for i, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
lines[i] = prefix + line
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// oidcClient mirrors an OpenCloud OIDC client (see services/idp default clients). All clients are
|
||||
// public/PKCE clients (no secret), matching the lico configuration.
|
||||
//
|
||||
// Note: Authelia (v4.39) has no per-client post-logout redirect URI list; RP-initiated logout
|
||||
// validates the redirect against the client's registered redirect_uris. The native clients below
|
||||
// therefore only need their redirect_uris registered.
|
||||
type oidcClient struct {
|
||||
ID string
|
||||
Name string
|
||||
ConsentMode string // "implicit" for trusted clients, "auto" otherwise
|
||||
RedirectURIs []string
|
||||
}
|
||||
|
||||
// defaultOIDCClients returns the default OpenCloud OIDC clients, mirroring the lico/idp defaults.
|
||||
// {{OC_URL}} placeholders are substituted with the configured OpenCloud URL.
|
||||
func defaultOIDCClients() []oidcClient {
|
||||
return []oidcClient{
|
||||
{
|
||||
ID: "web",
|
||||
Name: "OpenCloud Web App",
|
||||
ConsentMode: "implicit",
|
||||
RedirectURIs: []string{
|
||||
"{{OC_URL}}/",
|
||||
"{{OC_URL}}/oidc-callback.html",
|
||||
"{{OC_URL}}/oidc-silent-redirect.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "OpenCloudDesktop",
|
||||
Name: "OpenCloud Desktop Client",
|
||||
ConsentMode: "auto",
|
||||
RedirectURIs: []string{
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "OpenCloudAndroid",
|
||||
Name: "OpenCloud Android App",
|
||||
ConsentMode: "auto",
|
||||
RedirectURIs: []string{
|
||||
"oc://android.opencloud.eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "OpenCloudIOS",
|
||||
Name: "OpenCloud iOS App",
|
||||
ConsentMode: "auto",
|
||||
RedirectURIs: []string{
|
||||
"oc://ios.opencloud.eu",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// renderOIDCClients renders the OIDC clients list as YAML, indented to sit under
|
||||
// 'identity_providers.oidc.clients:'. List item dashes are at 6 spaces, fields at 8 spaces.
|
||||
func renderOIDCClients(ocURL string) string {
|
||||
var b strings.Builder
|
||||
for _, c := range defaultOIDCClients() {
|
||||
b.WriteString(" - client_id: '" + c.ID + "'\n")
|
||||
b.WriteString(" client_name: '" + c.Name + "'\n")
|
||||
b.WriteString(" public: true\n")
|
||||
b.WriteString(" authorization_policy: 'one_factor'\n")
|
||||
b.WriteString(" consent_mode: '" + c.ConsentMode + "'\n")
|
||||
b.WriteString(" require_pkce: true\n")
|
||||
b.WriteString(" pkce_challenge_method: 'S256'\n")
|
||||
b.WriteString(" token_endpoint_auth_method: 'none'\n")
|
||||
b.WriteString(" redirect_uris:\n")
|
||||
for _, uri := range c.RedirectURIs {
|
||||
b.WriteString(" - '" + strings.ReplaceAll(uri, "{{OC_URL}}", ocURL) + "'\n")
|
||||
}
|
||||
// 'offline_access' is required for the 'refresh_token' grant below; without it Authelia
|
||||
// rejects the grant (refresh tokens are how the web/desktop/mobile clients renew sessions).
|
||||
b.WriteString(" scopes:\n")
|
||||
b.WriteString(" - 'openid'\n")
|
||||
b.WriteString(" - 'profile'\n")
|
||||
b.WriteString(" - 'email'\n")
|
||||
b.WriteString(" - 'groups'\n")
|
||||
b.WriteString(" - 'offline_access'\n")
|
||||
b.WriteString(" grant_types:\n")
|
||||
b.WriteString(" - 'authorization_code'\n")
|
||||
b.WriteString(" - 'refresh_token'\n")
|
||||
b.WriteString(" response_types:\n")
|
||||
b.WriteString(" - 'code'\n")
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
// renderNotifier renders the 'notifier' YAML block. When an SMTP host is configured it renders an
|
||||
// SMTP notifier; otherwise it falls back to a filesystem notifier that writes mails to a file under
|
||||
// the data dir - so a fresh deployment works without any mail server. The notifier startup check is
|
||||
// disabled either way so an unreachable or flaky mail server never blocks startup.
|
||||
func renderNotifier(cfg *config.Config, domain string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("notifier:\n")
|
||||
b.WriteString(" disable_startup_check: true\n")
|
||||
|
||||
if cfg.SMTP.Host == "" {
|
||||
filename := filepath.Join(defaults.BaseDataPath(), "authelia", "notification.txt")
|
||||
b.WriteString(" # No SMTP host configured: notifications (password reset, 2FA registration) are\n")
|
||||
b.WriteString(" # written to this file. Configure SMTP (NOTIFICATIONS_SMTP_* / AUTH_AUTHELIA_SMTP_*)\n")
|
||||
b.WriteString(" # to send them by email instead.\n")
|
||||
b.WriteString(" filesystem:\n")
|
||||
b.WriteString(" filename: " + yamlSingleQuote(filename) + "\n")
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
sender := cfg.SMTP.Sender
|
||||
if sender == "" {
|
||||
sender = fmt.Sprintf("OpenCloud <no-reply@%s>", domain)
|
||||
}
|
||||
b.WriteString(" smtp:\n")
|
||||
b.WriteString(" address: " + yamlSingleQuote(smtpAddress(cfg.SMTP)) + "\n")
|
||||
b.WriteString(" sender: " + yamlSingleQuote(sender) + "\n")
|
||||
if cfg.SMTP.Username != "" {
|
||||
b.WriteString(" username: " + yamlSingleQuote(cfg.SMTP.Username) + "\n")
|
||||
}
|
||||
if cfg.SMTP.Password != "" {
|
||||
b.WriteString(" password: " + yamlSingleQuote(cfg.SMTP.Password) + "\n")
|
||||
}
|
||||
if strings.EqualFold(cfg.SMTP.Encryption, "none") {
|
||||
// No transport encryption: Authelia refuses to send credentials in the clear unless told to.
|
||||
b.WriteString(" disable_require_tls: true\n")
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
// smtpAddress builds an Authelia SMTP address (scheme://host[:port]) from the OpenCloud SMTP
|
||||
// settings. The scheme encodes the transport encryption: 'submissions' for implicit TLS (ssltls),
|
||||
// 'submission' for STARTTLS (starttls), and 'smtp' otherwise.
|
||||
func smtpAddress(s config.SMTP) string {
|
||||
scheme := "smtp"
|
||||
switch strings.ToLower(s.Encryption) {
|
||||
case "ssltls":
|
||||
scheme = "submissions"
|
||||
case "starttls":
|
||||
scheme = "submission"
|
||||
}
|
||||
addr := scheme + "://" + s.Host
|
||||
if s.Port > 0 {
|
||||
addr += ":" + strconv.Itoa(s.Port)
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// yamlSingleQuote wraps a value in a single-quoted YAML scalar, escaping embedded single quotes by
|
||||
// doubling them. Used for operator-supplied values (SMTP credentials, paths) that may contain
|
||||
// characters which are unsafe unquoted.
|
||||
func yamlSingleQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
||||
}
|
||||
|
||||
// secretsTemplate renders authelia.secrets.yaml. It contains only the persisted secrets; everything
|
||||
// else lives in the regenerated main config. Authelia deep-merges the two at load time.
|
||||
const secretsTemplate = `# GENERATED FILE - DO NOT EDIT.
|
||||
# Authelia secrets, generated once by the auth-authelia service and then left untouched so that
|
||||
# existing sessions and issued OIDC tokens remain valid across restarts and config changes.
|
||||
# Deleting this file makes the service generate new secrets on the next start, which invalidates all
|
||||
# sessions and OIDC tokens. Keep this file private (mode 0600).
|
||||
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: '{{ .ResetPasswordJWTSecret }}'
|
||||
|
||||
session:
|
||||
secret: '{{ .SessionSecret }}'
|
||||
|
||||
storage:
|
||||
encryption_key: '{{ .StorageEncryptionKey }}'
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: '{{ .OIDCHMACSecret }}'
|
||||
jwks:
|
||||
- key_id: 'default'
|
||||
algorithm: 'RS256'
|
||||
use: 'sig'
|
||||
key: |
|
||||
{{ .OIDCKeyPEMIndented }}
|
||||
`
|
||||
|
||||
// configTemplate renders authelia.yaml. It targets the Authelia v4.39 configuration schema and holds
|
||||
// only values derived from the OpenCloud configuration (no secrets) so it can be safely regenerated
|
||||
// on every start. The embedded Authelia provider validates the merged configuration on startup.
|
||||
const configTemplate = `# GENERATED FILE - DO NOT EDIT.
|
||||
# The auth-authelia service regenerates this file from the current OpenCloud configuration every time
|
||||
# it starts, so manual changes here are lost. Change these values through the OpenCloud configuration
|
||||
# instead (OC_URL, AUTH_AUTHELIA_* / NOTIFICATIONS_SMTP_* environment variables, ...).
|
||||
#
|
||||
# Secrets are kept separately in 'authelia.secrets.yaml' (generated once).
|
||||
# To set or override options that OpenCloud does not manage, create 'authelia.override.yaml' in this
|
||||
# directory: it is merged last (takes precedence) and is never modified by OpenCloud.
|
||||
|
||||
server:
|
||||
address: '{{ .Address }}'
|
||||
|
||||
log:
|
||||
level: '{{ .LogLevel }}'
|
||||
|
||||
theme: 'auto'
|
||||
|
||||
totp:
|
||||
issuer: '{{ .Domain }}'
|
||||
|
||||
authentication_backend:
|
||||
ldap:
|
||||
implementation: 'custom'
|
||||
address: '{{ .LDAPAddress }}'
|
||||
tls:
|
||||
# The embedded libregraph-idm LDAPS server uses a self-signed certificate on localhost.
|
||||
skip_verify: true
|
||||
base_dn: 'o=libregraph-idm'
|
||||
additional_users_dn: 'ou=users'
|
||||
additional_groups_dn: 'ou=groups'
|
||||
users_filter: '(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=inetOrgPerson))'
|
||||
groups_filter: '(&(member={dn})(objectClass=groupOfNames))'
|
||||
user: '{{ .LDAPBindDN }}'
|
||||
password: '{{ .LDAPPassword }}'
|
||||
attributes:
|
||||
username: 'uid'
|
||||
display_name: 'displayName'
|
||||
mail: 'mail'
|
||||
group_name: 'cn'
|
||||
|
||||
access_control:
|
||||
default_policy: 'one_factor'
|
||||
|
||||
session:
|
||||
cookies:
|
||||
- domain: '{{ .Domain }}'
|
||||
authelia_url: '{{ .AutheliaURL }}'
|
||||
|
||||
storage:
|
||||
local:
|
||||
path: '{{ .StoragePath }}'
|
||||
|
||||
# NTP is only used to detect clock skew for TOTP. The startup connectivity check is disabled so the
|
||||
# embedded service does not require outbound NTP to boot.
|
||||
ntp:
|
||||
disable_startup_check: true
|
||||
|
||||
{{ .NotifierYAML }}
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
clients:
|
||||
{{ .OIDCClientsYAML }}
|
||||
`
|
||||
@@ -40,9 +40,18 @@ func DefaultConfig() *config.Config {
|
||||
Name: "proxy",
|
||||
},
|
||||
OIDC: config.OIDC{
|
||||
Issuer: "https://localhost:9200",
|
||||
// The default IdP is the embedded Authelia provider, served under the '/authelia' base path,
|
||||
// so the issuer is '<OC_URL>/authelia'. In supervised mode the runtime sets OC_OIDC_ISSUER
|
||||
// accordingly; this literal covers the localhost default when no issuer env is set.
|
||||
Issuer: "https://localhost:9200/authelia",
|
||||
|
||||
AccessTokenVerifyMethod: config.AccessTokenVerificationJWT,
|
||||
// Authelia (the default IdP) issues opaque access tokens, not JWTs, so the proxy cannot
|
||||
// verify them locally against the JWKS ('jwt' method). 'none' skips local JWT verification;
|
||||
// tokens are instead validated against Authelia's /userinfo endpoint (SkipUserInfo is false
|
||||
// below, and the result is cached). This is validation by the IdP, not a bypass. Switch to
|
||||
// 'jwt' only if the IdP issues JWT access tokens (e.g. lico, or Authelia clients configured
|
||||
// with access_token_signed_response_alg).
|
||||
AccessTokenVerifyMethod: config.AccessTokenVerificationNone,
|
||||
SkipUserInfo: false,
|
||||
UserinfoCache: &config.Cache{
|
||||
Store: "memory",
|
||||
@@ -128,23 +137,18 @@ func DefaultPolicies() []config.Policy {
|
||||
Service: "eu.opencloud.web.webfinger",
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "/.well-known/openid-configuration",
|
||||
Service: "eu.opencloud.web.idp",
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "/branding/logo",
|
||||
Service: "eu.opencloud.web.web",
|
||||
},
|
||||
{
|
||||
Endpoint: "/konnect/",
|
||||
Service: "eu.opencloud.web.idp",
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
Endpoint: "/signin/",
|
||||
Service: "eu.opencloud.web.idp",
|
||||
// The embedded Authelia provider (auth-authelia service) is the default IdP. It serves
|
||||
// its login portal and all OIDC endpoints (incl. /authelia/.well-known/openid-configuration)
|
||||
// under the '/authelia' base path on its own HTTP listener; this route forwards them to it.
|
||||
// The OIDC issuer is '<OC_URL>/authelia'. When falling back to the lico 'idp' service,
|
||||
// add the '/konnect/' and '/signin/' routes (and set the issuer to '<OC_URL>') instead.
|
||||
Endpoint: "/authelia",
|
||||
Backend: "http://127.0.0.1:9091",
|
||||
Unprotected: true,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user