-
Notifications
You must be signed in to change notification settings - Fork 2
Support source authentication using client certificates #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
821a564
52d816c
83b4818
3a319d3
ac0edbb
92c3f22
283e301
9fa040e
1d60d2b
472cc0d
5652b43
4321331
461e842
84ca131
553e608
6f5ce47
d085910
064ba36
431560c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,11 +125,14 @@ func (l *Listener) Run(ctx context.Context) error { | |
| ErrorLog: stdlogger, | ||
| } | ||
|
|
||
| ctx, cancel := context.WithCancelCause(ctx) | ||
| defer cancel(nil) | ||
|
|
||
| var listeningFunc func() error | ||
| if l.useSocket { | ||
| listeningFunc, err = l.initSocketServer(server) | ||
| } else { | ||
| listeningFunc, err = l.initTcpServer(server) | ||
| listeningFunc, err = l.initTcpServer(ctx, cancel, server) | ||
| } | ||
| if err != nil { | ||
| return err | ||
|
|
@@ -142,9 +145,12 @@ func (l *Listener) Run(ctx context.Context) error { | |
|
|
||
| select { | ||
| case <-ctx.Done(): | ||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) | ||
| defer cancel() | ||
| return server.Shutdown(shutdownCtx) | ||
| shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 3*time.Second) | ||
| defer shutdownCancel() | ||
| if err := server.Shutdown(shutdownCtx); err != nil { | ||
| return err | ||
| } | ||
| return context.Cause(ctx) | ||
|
|
||
| case err := <-serverErr: | ||
| return err | ||
|
|
@@ -153,13 +159,23 @@ func (l *Listener) Run(ctx context.Context) error { | |
|
|
||
| // initTcpServer configures the given HTTP(S) server for the configured TCP address and returns a function that | ||
| // starts serving. The caller is responsible for actually calling that function and for graceful shutdown. | ||
| func (l *Listener) initTcpServer(server *http.Server) (func() error, error) { | ||
| func (l *Listener) initTcpServer(ctx context.Context, cancel context.CancelCauseFunc, server *http.Server) (func() error, error) { | ||
| conf := daemon.Config().Listener | ||
| tlsConfig, err := conf.GetTlsConfig() | ||
| tlsConfig, crlChecker, err := conf.GetTlsConfig() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just saw that this was already addressed in d085910 shortly before I sent this review. Nevertheless, please make the API less dangerous: In the IGL, return an error if |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| go func() { | ||
| err := crlChecker.WatchAndReload(ctx, l.logger.SugaredLogger) | ||
| if ctx.Err() != nil || err == nil { | ||
| // WatchAndReload returning is expected (Context was cancelled externally or noop). | ||
| return | ||
| } | ||
| l.logger.Errorw("CRL watcher exited unexpectedly", zap.Error(err)) | ||
| cancel(fmt.Errorf("CRL watcher exited: %w", err)) | ||
| }() | ||
|
|
||
| var https string | ||
| if conf.TLSOptions.Enable { | ||
| https = "s" | ||
|
|
@@ -274,45 +290,98 @@ func (l *Listener) initSocketServer(server *http.Server) (fn func() error, retEr | |
| // up by the OS username of the connecting process via peer credentials. For TCP connections, HTTP Basic Auth is used. | ||
| // If no matching source is found, nil is returned and 401 is written to the response. | ||
| func (l *Listener) sourceFromAuthOrAbort(w http.ResponseWriter, r *http.Request) (src *config.Source) { | ||
| errFunc := func(errMsg string) *config.Source { | ||
| w.WriteHeader(http.StatusUnauthorized) | ||
| var errMsg string | ||
| errFunc := func(httpCode int, sendAuthHeader bool) *config.Source { | ||
| if sendAuthHeader { | ||
| w.Header().Set("WWW-Authenticate", `Basic realm="icinga-notifications source"`) | ||
| } | ||
| l.logger.Errorw("Error in sourceFromAuthOrAbort", zap.String("error", errMsg)) | ||
| w.WriteHeader(httpCode) | ||
| _, _ = fmt.Fprintln(w, errMsg) | ||
| return nil | ||
| } | ||
|
|
||
| if l.useSocket { | ||
| l.logger.Debugw("Source is authenticated via socket connection") | ||
| errMsg = "unix socket: " | ||
| value := r.Context().Value(peerUserLookupKey{}) | ||
| if value == nil { | ||
| return errFunc("no value found in context") | ||
| errMsg += "no client certificate found" | ||
| return errFunc(http.StatusUnauthorized, false) | ||
| } | ||
|
|
||
| lookup, ok := value.(peerUserLookupFunc) | ||
| if !ok { | ||
| return errFunc("no lookup function present") | ||
| errMsg += "no lookup function present" | ||
| return errFunc(http.StatusUnauthorized, false) | ||
| } | ||
|
|
||
| username, err := lookup() | ||
| if err != nil { | ||
| return errFunc(err.Error()) | ||
| errMsg += err.Error() | ||
| return errFunc(http.StatusUnauthorized, false) | ||
| } | ||
|
|
||
| src = l.runtimeConfig.GetSourceByUsername(username) | ||
| if src == nil { | ||
| return errFunc(fmt.Sprintf("system user %q is not registered as a source username", username)) | ||
| errMsg += fmt.Sprintf("system user %q is not registered as a source username", username) | ||
| return errFunc(http.StatusUnauthorized, false) | ||
| } | ||
|
|
||
| l.logger.Debugw( | ||
| "Source is authenticated via socket connection", | ||
| zap.String("source_name", src.Name), | ||
| zap.String("username", username), | ||
| ) | ||
| } else { | ||
| l.logger.Debugw("Source is authenticated via HTTP Basic Auth") | ||
| authUser, authPass, authOk := r.BasicAuth() | ||
| if !authOk { | ||
| w.Header().Set("WWW-Authenticate", `Basic realm="icinga-notifications source"`) | ||
| return errFunc("missing or malformed basic auth credentials") | ||
| } | ||
| if r.TLS != nil && len(r.TLS.VerifiedChains) > 0 { | ||
| // if VerifiedChains isn't empty, the request doesn't fall back to basic auth | ||
| errMsg = "tls cert: " | ||
| if authUser != "" || authPass != "" { | ||
| errMsg += " both client certificate and basic auth provided" | ||
| return errFunc(http.StatusBadRequest, false) | ||
| } | ||
|
|
||
| src = l.runtimeConfig.GetSourceFromCredentials(authUser, authPass, l.logger) | ||
| if src == nil { | ||
| w.Header().Set("WWW-Authenticate", `Basic realm="icinga-notifications source"`) | ||
| return errFunc("expected valid icinga-notifications source basic auth credentials") | ||
| if len(r.TLS.VerifiedChains[0]) == 0 { | ||
| errMsg += "no verified chain found" | ||
| return errFunc(http.StatusUnauthorized, true) | ||
| } | ||
|
|
||
| clientCert := r.TLS.VerifiedChains[0][0] | ||
| if clientCert == nil { | ||
| errMsg += "no client certificate found" | ||
| return errFunc(http.StatusUnauthorized, true) | ||
| } | ||
|
|
||
| src = l.runtimeConfig.GetSourceByClientCertSubject(clientCert.Subject.String()) | ||
| if src == nil { | ||
| errMsg += "no matching source found" | ||
| return errFunc(http.StatusUnauthorized, true) | ||
| } | ||
|
|
||
| l.logger.Debugw( | ||
| "Source is authenticated via a TLS client certificate", | ||
| zap.String("source_name", src.Name), | ||
| zap.String("common_name", clientCert.Subject.CommonName), | ||
| ) | ||
| } else { | ||
| errMsg = "basic auth: " | ||
| if !authOk { | ||
| errMsg += "missing or malformed basic auth credentials" | ||
| return errFunc(http.StatusUnauthorized, true) | ||
| } | ||
|
|
||
| src = l.runtimeConfig.GetSourceFromCredentials(authUser, authPass, l.logger) | ||
| if src == nil { | ||
| errMsg += "expected valid icinga-notifications source basic auth credentials" | ||
| return errFunc(http.StatusUnauthorized, true) | ||
| } | ||
|
|
||
| l.logger.Debugw( | ||
| "Source is authenticated via HTTP Basic Auth", | ||
| zap.String("source_name", src.Name), | ||
| zap.String("username", authUser), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.