From 9b1afe4a7c84fbc5365b7f83e6eabc926bb508cb Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 19 Aug 2023 10:49:51 -0700 Subject: Add prometheus metrics --- app/controllers/ca.go | 22 ++++++++++++++++++++++ app/controllers/login.go | 20 ++++++++++++++++++++ app/controllers/oauth2_device.go | 28 ++++++++++++++++++++++++++++ app/controllers/proxy.go | 19 +++++++++++++++++++ app/controllers/register.go | 22 ++++++++++++++++++++++ go.mod | 2 +- 6 files changed, 112 insertions(+), 1 deletion(-) diff --git a/app/controllers/ca.go b/app/controllers/ca.go index 632db50..c04dcd8 100644 --- a/app/controllers/ca.go +++ b/app/controllers/ca.go @@ -11,9 +11,24 @@ import ( "code.crute.us/mcrute/ssh-proxy/app/middleware" "code.crute.us/mcrute/ssh-proxy/app/models" "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "golang.org/x/crypto/ssh" ) +var ( + caError = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "ssh_ca_error", + Help: "Total number of errors during SSH CA operation", + }, []string{"type"}) + caSuccess = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "ssh_ca_success", + Help: "Total number of successful CA operations", + }) +) + type CASecret struct { Key string `mapstructure:"key"` } @@ -57,24 +72,29 @@ func (h *CAHandler) authorizeRequest(c echo.Context, certRequest *ssh.Certificat } if user.Username != certRequest.ValidPrincipals[0] { + caError.With(prometheus.Labels{"type": "user_request_mismatch"}).Inc() return fmt.Errorf("Authenticated username and cert username must match") } if !session.HasScope("ca:issue") { + caError.With(prometheus.Labels{"type": "missing_oauth_scope"}).Inc() return fmt.Errorf("Authorized session does not have scope ca:issue") } if certRequest.Extensions == nil { + caError.With(prometheus.Labels{"type": "no_extensions"}).Inc() return fmt.Errorf("Cert request extensions are empty") } hostLine, ok := certRequest.Extensions["allowed-hosts"] if !ok { + caError.With(prometheus.Labels{"type": "no_allowed_hosts"}).Inc() return fmt.Errorf("Cert request allowed-hosts is blank") } for _, host := range strings.Split(hostLine, ",") { if !user.AuthorizedForHost(host) { + caError.With(prometheus.Labels{"type": "user_no_auth_host"}).Inc() return fmt.Errorf("User %s is not authorized for host %s", session.UserId, host) } } @@ -168,5 +188,7 @@ func (h *CAHandler) HandleIssue(c echo.Context) error { }) } + caSuccess.Inc() + return c.Blob(http.StatusOK, "application/x-ssh-certificate", ssh.MarshalAuthorizedKey(certToIssue)) } diff --git a/app/controllers/login.go b/app/controllers/login.go index 603eb20..f59789f 100644 --- a/app/controllers/login.go +++ b/app/controllers/login.go @@ -13,6 +13,21 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + loginError = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "login_error", + Help: "Total number of errors during login operation", + }, []string{"type"}) + loginSuccess = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "login_success", + Help: "Total number of successful logins", + }) ) type LoginController[T app.AppSession] struct { @@ -57,6 +72,7 @@ func (a *LoginController[T]) HandleFinish(c echo.Context) error { user, err := a.Users.Get(ctx, c.Param("username")) if err != nil { a.Logger.Errorf("Error getting user: %s", err) + loginError.With(prometheus.Labels{"type": "no_user"}).Inc() return c.NoContent(http.StatusNotFound) } @@ -76,6 +92,7 @@ func (a *LoginController[T]) HandleFinish(c echo.Context) error { if _, err := a.Webauthn.ValidateLogin(user, *s.WebauthnSession, response); err != nil { a.Logger.Errorf("Error validating login: %s", err) + loginError.With(prometheus.Labels{"type": "webauthn_invalid"}).Inc() return c.NoContent(http.StatusBadRequest) } @@ -96,11 +113,13 @@ func (a *LoginController[T]) HandleFinish(c echo.Context) error { authSession, err := a.AuthSessions.GetByUserCode(ctx, code.Code) if err != nil { a.Logger.Errorf("No auth session exists") + loginError.With(prometheus.Labels{"type": "no_session_for_code"}).Inc() return c.NoContent(http.StatusUnauthorized) } if authSession.AccessCode != "" { a.Logger.Errorf("Session is already authenticated") + loginError.With(prometheus.Labels{"type": "already_authenticated"}).Inc() return c.NoContent(http.StatusUnauthorized) } @@ -113,5 +132,6 @@ func (a *LoginController[T]) HandleFinish(c echo.Context) error { return c.NoContent(http.StatusInternalServerError) } + loginSuccess.Inc() return c.NoContent(http.StatusOK) } diff --git a/app/controllers/oauth2_device.go b/app/controllers/oauth2_device.go index 0ddf653..c431495 100644 --- a/app/controllers/oauth2_device.go +++ b/app/controllers/oauth2_device.go @@ -9,7 +9,23 @@ import ( "code.crute.us/mcrute/ssh-proxy/app" "code.crute.us/mcrute/ssh-proxy/app/models" + "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + oauth2DeviceError = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "oauth2_device_error", + Help: "Total number of errors during oauth2 device operations", + }, []string{"type"}) + oauth2DeviceSuccess = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "oauth2_device_success", + Help: "Total number of successful oauth2 device auths", + }) ) func badRequest(c echo.Context, e models.AuthorizationError, d string) error { @@ -40,15 +56,18 @@ func (a *OAuth2DeviceController[T]) HandleStart(c echo.Context) error { client, err := a.OauthClients.Get(ctx, form.ClientId) if err != nil { a.Logger.Errorf("Unable to find client ID '%s': %s", form.ClientId, err) + oauth2DeviceError.With(prometheus.Labels{"type": "invalid_client_id"}).Inc() return badRequest(c, models.ErrUnauthorizedClient, "") } if len(form.Challenge) <= 16 { + oauth2DeviceError.With(prometheus.Labels{"type": "challenge_length"}).Inc() return badRequest(c, models.ErrInvalidRequest, "code_challenge is too short, minimum length is 16 bytes") } if form.ChallengeMethod != models.ChallengeS256 { + oauth2DeviceError.With(prometheus.Labels{"type": "challenge_type"}).Inc() return badRequest(c, models.ErrInvalidRequest, "code_challenge_method invalid, only S256 supported") } @@ -58,11 +77,13 @@ func (a *OAuth2DeviceController[T]) HandleStart(c echo.Context) error { session.SetScopeString(form.Scope) if !session.HasAnyScopes() { + oauth2DeviceError.With(prometheus.Labels{"type": "no_scopes"}).Inc() return badRequest(c, models.ErrInvalidRequest, "one or more scopes required") } for _, s := range session.Scope { if s != "ssh:proxy" && s != "ca:issue" { + oauth2DeviceError.With(prometheus.Labels{"type": "invalid_scope"}).Inc() return badRequest(c, models.ErrInvalidScope, fmt.Sprintf("scope %s is not recognized", s)) } } @@ -93,27 +114,33 @@ func (a *OAuth2DeviceController[T]) HandleToken(c echo.Context) error { session, err := a.AuthSessions.Get(ctx, form.DeviceCode) if err != nil { + oauth2DeviceError.With(prometheus.Labels{"type": "no_auth_session"}).Inc() return c.NoContent(http.StatusNotFound) } if form.GrantType != models.DEVICE_CODE_GRANT_TYPE { + oauth2DeviceError.With(prometheus.Labels{"type": "invalid_grant_type"}).Inc() return badRequest(c, models.ErrUnsupportedGrantType, "") } if subtle.ConstantTimeCompare([]byte(session.ClientId), []byte(form.ClientId)) != 1 { + oauth2DeviceError.With(prometheus.Labels{"type": "client_id_mismatch"}).Inc() return badRequest(c, models.ErrUnauthorizedClient, "") } if time.Now().After(session.Expires) { + oauth2DeviceError.With(prometheus.Labels{"type": "expired_session"}).Inc() return badRequest(c, models.ErrExpiredToken, "") } verifier := &models.PKCEChallenge{Verifier: form.CodeVerifier} if verifier.EqualString(session.Challenge) { + oauth2DeviceError.With(prometheus.Labels{"type": "pkce_mismatch"}).Inc() return badRequest(c, models.ErrInvalidGrant, "") // Per RFC7636 4.6 } if session.IsRegistration { + oauth2DeviceError.With(prometheus.Labels{"type": "is_registration_session"}).Inc() return badRequest(c, models.ErrInvalidGrant, "") } @@ -121,6 +148,7 @@ func (a *OAuth2DeviceController[T]) HandleToken(c echo.Context) error { return badRequest(c, models.ErrAuthorizationPending, "") } + oauth2DeviceSuccess.Inc() return c.JSON(http.StatusOK, models.AccessTokenResponse{ AccessToken: session.AccessCode, TokenType: "Bearer", diff --git a/app/controllers/proxy.go b/app/controllers/proxy.go index c8345e8..9e3ec13 100644 --- a/app/controllers/proxy.go +++ b/app/controllers/proxy.go @@ -12,6 +12,21 @@ import ( "github.com/gorilla/websocket" "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + proxyError = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "proxy_error", + Help: "Total number of errors during proxy setup operation", + }, []string{"type"}) + proxySuccess = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "proxy_success", + Help: "Total number of successful proxy sessions", + }) ) type ProxyHandler struct { @@ -37,6 +52,7 @@ func (h *ProxyHandler) authorizeRequest(c echo.Context) error { } if !session.HasScope("ssh:proxy") { + proxyError.With(prometheus.Labels{"type": "token_missing_scope"}).Inc() return fmt.Errorf("Authorized session does not have scope ssh:proxy") } @@ -46,6 +62,7 @@ func (h *ProxyHandler) authorizeRequest(c echo.Context) error { return nil } + proxyError.With(prometheus.Labels{"type": "not_authorized"}).Inc() return fmt.Errorf("User %s not authorized for host %s", session.UserId, host) } @@ -70,6 +87,8 @@ func (h *ProxyHandler) Handle(c echo.Context) error { errc := make(chan error) ws := &proxy.WebsocketReadWriter{W: wsconn} + proxySuccess.Inc() + go proxy.CopyWithErrors(proxyconn, ws, errc) go proxy.CopyWithErrors(ws, proxyconn, errc) diff --git a/app/controllers/register.go b/app/controllers/register.go index 7c1a0f3..312daae 100644 --- a/app/controllers/register.go +++ b/app/controllers/register.go @@ -15,6 +15,21 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + registerError = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "register_error", + Help: "Total number of errors during registration", + }, []string{"type"}) + registerSuccess = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "ssh_proxy", + Name: "register_success", + Help: "Total number of successful registrations", + }) ) type RegisterController[T app.AppSession] struct { @@ -32,18 +47,22 @@ func (a *RegisterController[T]) validateRequest(ctx context.Context, u *models.U authSession, err := a.AuthSessions.GetByUserCode(ctx, code) if err != nil { + registerError.With(prometheus.Labels{"type": "no_user_for_code"}).Inc() return nil, fmt.Errorf("No auth session exists") } if time.Now().After(authSession.Expires) { + registerError.With(prometheus.Labels{"type": "session_expired"}).Inc() return nil, fmt.Errorf("Session is expired") } if !authSession.IsRegistration { + registerError.With(prometheus.Labels{"type": "incorrect_session_type"}).Inc() return nil, fmt.Errorf("Session is not an invitation to register") } if authSession.UserId != u.Username { + registerError.With(prometheus.Labels{"type": "username_mismatch"}).Inc() return nil, fmt.Errorf("Session not valid for this user") } @@ -56,6 +75,7 @@ func (a *RegisterController[T]) HandleStart(c echo.Context) error { user, err := a.Users.Get(ctx, c.Param("username")) if err != nil { a.Logger.Errorf("Error getting user: %s", err) + registerError.With(prometheus.Labels{"type": "no_user"}).Inc() return c.NoContent(http.StatusNotFound) } @@ -111,6 +131,7 @@ func (a *RegisterController[T]) HandleFinish(c echo.Context) error { // session in case of other errors if err := a.AuthSessions.Delete(ctx, authSession); err != nil { a.Logger.Errorf("Error deleting auth session: %s", err) + registerError.With(prometheus.Labels{"type": "db_delete_session"}).Inc() return c.NoContent(http.StatusInternalServerError) } @@ -141,5 +162,6 @@ func (a *RegisterController[T]) HandleFinish(c echo.Context) error { return c.NoContent(http.StatusInternalServerError) } + registerSuccess.Inc() return c.NoContent(http.StatusOK) } diff --git a/go.mod b/go.mod index a416ec6..591f27c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/labstack/echo/v4 v4.6.1 github.com/mdp/qrterminal v1.0.1 + github.com/prometheus/client_golang v1.11.0 github.com/spf13/cobra v1.7.0 go.mongodb.org/mongo-driver v1.7.4 golang.org/x/crypto v0.11.0 @@ -75,7 +76,6 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.6.0 // indirect -- cgit v1.2.3