@@ -120,6 +120,10 @@ type Daemon struct {
120120 oidcVerifier * authnoidc.Verifier
121121 authorizer * authz.Authorizer
122122
123+ // securityRetryCancel stops the background connection retries of the
124+ // security infrastructure from the previous configuration.
125+ securityRetryCancel context.CancelFunc
126+
123127 server * http.Server
124128 listener * listener.FancyTLSListener
125129
@@ -188,6 +192,28 @@ func (d *Daemon) Start(ctx context.Context) error {
188192 slog .ErrorContext (ctx , "Failed to load security config" , logger .Err (err ))
189193 }
190194
195+ // On update of the OIDC or OpenFGA configuration, verify the configured
196+ // services are reachable, so a broken configuration update is rejected
197+ // right away. Connection failures after a successful update are handled
198+ // by background retries.
199+ lifecycle .SecurityValidateSignal .AddListenerWithErr (func (ctx context.Context , cfg apisystem.Security ) error {
200+ if cfg .OIDC .Issuer != "" && cfg .OIDC .ClientID != "" {
201+ err := authnoidc .CheckConnectivity (ctx , cfg .OIDC .Issuer )
202+ if err != nil {
203+ return fmt .Errorf ("Failed to reach OIDC issuer %q: %w" , cfg .OIDC .Issuer , err )
204+ }
205+ }
206+
207+ if cfg .OpenFGA .APIURL != "" && cfg .OpenFGA .APIToken != "" && cfg .OpenFGA .StoreID != "" {
208+ err := authzopenfga .CheckConnectivity (ctx , cfg .OpenFGA .APIURL , cfg .OpenFGA .APIToken , cfg .OpenFGA .StoreID )
209+ if err != nil {
210+ return fmt .Errorf ("Failed to reach OpenFGA at %q: %w" , cfg .OpenFGA .APIURL , err )
211+ }
212+ }
213+
214+ return nil
215+ })
216+
191217 // On update of the security configuration, perform reload of the security
192218 // related infrastructure.
193219 lifecycle .SecurityUpdateSignal .AddListener (func (ctx context.Context , cfg apisystem.Security ) {
@@ -467,6 +493,16 @@ func (d *Daemon) securityConfigReload(ctx context.Context, cfg apisystem.Securit
467493 d .configReloadMu .Lock ()
468494 defer d .configReloadMu .Unlock ()
469495
496+ // Cancel background connection retries from the previous configuration.
497+ if d .securityRetryCancel != nil {
498+ d .securityRetryCancel ()
499+ }
500+
501+ // Detached context for background connection retries, which needs to
502+ // outlive ctx, e.g. if the reload is triggered by an API request.
503+ retryCtx , retryCancel := context .WithCancel (logger .DetachedContext (ctx ))
504+ d .securityRetryCancel = retryCancel
505+
470506 var errs []error
471507
472508 // UnixSocket authenticator is always available.
@@ -477,7 +513,7 @@ func (d *Daemon) securityConfigReload(ctx context.Context, cfg apisystem.Securit
477513 // Setup OIDC authentication.
478514 if cfg .OIDC .Issuer != "" && cfg .OIDC .ClientID != "" {
479515 var err error
480- newOIDCVerifier , err := authnoidc .NewVerifier (context . TODO () , cfg .OIDC .Issuer , cfg .OIDC .ClientID , cfg .OIDC .Scope , cfg .OIDC .Audience , cfg .OIDC .Claim )
516+ newOIDCVerifier , err := authnoidc .NewVerifier (retryCtx , cfg .OIDC .Issuer , cfg .OIDC .ClientID , cfg .OIDC .Scope , cfg .OIDC .Audience , cfg .OIDC .Claim )
481517 if err != nil {
482518 errs = append (errs , err )
483519 } else {
@@ -509,7 +545,7 @@ func (d *Daemon) securityConfigReload(ctx context.Context, cfg apisystem.Securit
509545 }
510546
511547 if cfg .OpenFGA .APIURL != "" && cfg .OpenFGA .APIToken != "" && cfg .OpenFGA .StoreID != "" {
512- openfgaAuthorizer , err := authzopenfga .New (ctx , cfg .OpenFGA .APIURL , cfg .OpenFGA .APIToken , cfg .OpenFGA .StoreID )
548+ openfgaAuthorizer , err := authzopenfga .New (retryCtx , cfg .OpenFGA .APIURL , cfg .OpenFGA .APIToken , cfg .OpenFGA .StoreID )
513549 if err != nil {
514550 errs = append (errs , err )
515551 } else {
@@ -1717,6 +1753,13 @@ func (d *Daemon) incusOSSelfPoll(ctx context.Context, serverSvc provisioning.Ser
17171753}
17181754
17191755func (d * Daemon ) Stop (ctx context.Context ) error {
1756+ d .configReloadMu .Lock ()
1757+ if d .securityRetryCancel != nil {
1758+ d .securityRetryCancel ()
1759+ }
1760+
1761+ d .configReloadMu .Unlock ()
1762+
17201763 errs := make ([]error , 0 , len (d .shutdownFuncs )+ 1 )
17211764
17221765 for _ , shutdown := range d .shutdownFuncs {
@@ -1732,6 +1775,7 @@ func (d *Daemon) Stop(ctx context.Context) error {
17321775 // Remove all signal listeners.
17331776 lifecycle .ServerCertificateUpdateSignal .Reset ()
17341777 lifecycle .NetworkUpdateSignal .Reset ()
1778+ lifecycle .SecurityValidateSignal .Reset ()
17351779 lifecycle .SecurityUpdateSignal .Reset ()
17361780 lifecycle .SecurityTrustedHTTPSProxiesUpdateSignal .Reset ()
17371781 lifecycle .SecurityACMEUpdateSignal .Reset ()
0 commit comments