diff --git a/authority/authority.go b/authority/authority.go index 95e00a45..c112bc25 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -104,6 +104,9 @@ type Authority struct { // If true, do not output initialization logs quietInit bool + + // Called whenever applicable, in order to instrument the authority. + meter Meter } // Info contains information about the authority. @@ -126,6 +129,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { config: cfg, certificates: new(sync.Map), validateSCEP: true, + meter: noopMeter{}, } // Apply options. @@ -134,6 +138,9 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { return nil, err } } + if a.keyManager != nil { + a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter} + } if !a.skipInit { // Initialize authority from options or configuration. @@ -151,6 +158,7 @@ func NewEmbedded(opts ...Option) (*Authority, error) { a := &Authority{ config: &config.Config{}, certificates: new(sync.Map), + meter: noopMeter{}, } // Apply options. @@ -159,6 +167,9 @@ func NewEmbedded(opts ...Option) (*Authority, error) { return nil, err } } + if a.keyManager != nil { + a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter} + } // Validate required options switch { @@ -337,6 +348,8 @@ func (a *Authority) init() error { if err != nil { return err } + + a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter} } // Initialize linkedca client if necessary. On a linked RA, the issuer diff --git a/authority/authorize.go b/authority/authorize.go index f14574a8..02147687 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -286,16 +286,16 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { // extra extension cannot be found, authorize the renewal by default. // // TODO(mariano): should we authorize by default? -func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error { +func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) (provisioner.Interface, error) { serial := cert.SerialNumber.String() var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)} isRevoked, err := a.IsRevoked(serial) if err != nil { - return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) } if isRevoked { - return errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...) + return nil, errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...) } p, err := a.LoadProvisionerByCertificate(cert) if err != nil { @@ -305,13 +305,13 @@ func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) // returns the noop provisioner if this happens, and it allows // certificate renewals. if p, ok = a.provisioners.LoadByCertificate(cert); !ok { - return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) + return nil, errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) } } if err := p.AuthorizeRenew(ctx, cert); err != nil { - return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) } - return nil + return p, nil } // authorizeSSHCertificate returns an error if the given certificate is revoked. diff --git a/authority/authorize_test.go b/authority/authorize_test.go index bec34fd6..3d748f69 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -876,7 +876,7 @@ func TestAuthority_authorizeRenew(t *testing.T) { t.Run(name, func(t *testing.T) { tc := genTestCase(t) - err := tc.auth.authorizeRenew(context.Background(), tc.cert) + _, err := tc.auth.authorizeRenew(context.Background(), tc.cert) if err != nil { if assert.NotNil(t, tc.err) { var sc render.StatusCodedError diff --git a/authority/config/config.go b/authority/config/config.go index ba581d8a..ea7ce35d 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -83,6 +83,7 @@ type Config struct { Templates *templates.Templates `json:"templates,omitempty"` CommonName string `json:"commonName,omitempty"` CRL *CRLConfig `json:"crl,omitempty"` + MetricsAddress string `json:"metricsAddress,omitempty"` SkipValidation bool `json:"-"` // Keeps record of the filename the Config is read from @@ -327,6 +328,12 @@ func (c *Config) Validate() error { return errors.Errorf("invalid address %s", c.Address) } + if addr := c.MetricsAddress; addr != "" { + if _, _, err := net.SplitHostPort(addr); err != nil { + return errors.Errorf("invalid metrics address %q", c.Address) + } + } + if c.TLS == nil { c.TLS = &DefaultTLSOptions } else { diff --git a/authority/meter.go b/authority/meter.go new file mode 100644 index 00000000..cccda22a --- /dev/null +++ b/authority/meter.go @@ -0,0 +1,87 @@ +package authority + +import ( + "crypto" + "io" + + "go.step.sm/crypto/kms" + kmsapi "go.step.sm/crypto/kms/apiv1" + + "github.com/smallstep/certificates/authority/provisioner" +) + +// Meter wraps the set of defined callbacks for metrics gatherers. +type Meter interface { + // X509Signed is called whenever an X509 certificate is signed. + X509Signed(provisioner.Interface, error) + + // X509Renewed is called whenever an X509 certificate is renewed. + X509Renewed(provisioner.Interface, error) + + // X509Rekeyed is called whenever an X509 certificate is rekeyed. + X509Rekeyed(provisioner.Interface, error) + + // X509WebhookAuthorized is called whenever an X509 authoring webhook is called. + X509WebhookAuthorized(provisioner.Interface, error) + + // X509WebhookEnriched is called whenever an X509 enriching webhook is called. + X509WebhookEnriched(provisioner.Interface, error) + + // SSHSigned is called whenever an SSH certificate is signed. + SSHSigned(provisioner.Interface, error) + + // SSHRenewed is called whenever an SSH certificate is renewed. + SSHRenewed(provisioner.Interface, error) + + // SSHRekeyed is called whenever an SSH certificate is rekeyed. + SSHRekeyed(provisioner.Interface, error) + + // SSHWebhookAuthorized is called whenever an SSH authoring webhook is called. + SSHWebhookAuthorized(provisioner.Interface, error) + + // SSHWebhookEnriched is called whenever an SSH enriching webhook is called. + SSHWebhookEnriched(provisioner.Interface, error) + + // KMSSigned is called per KMS signer signature. + KMSSigned(error) +} + +// noopMeter implements a noop [Meter]. +type noopMeter struct{} + +func (noopMeter) SSHRekeyed(provisioner.Interface, error) {} +func (noopMeter) SSHRenewed(provisioner.Interface, error) {} +func (noopMeter) SSHSigned(provisioner.Interface, error) {} +func (noopMeter) SSHWebhookAuthorized(provisioner.Interface, error) {} +func (noopMeter) SSHWebhookEnriched(provisioner.Interface, error) {} +func (noopMeter) X509Rekeyed(provisioner.Interface, error) {} +func (noopMeter) X509Renewed(provisioner.Interface, error) {} +func (noopMeter) X509Signed(provisioner.Interface, error) {} +func (noopMeter) X509WebhookAuthorized(provisioner.Interface, error) {} +func (noopMeter) X509WebhookEnriched(provisioner.Interface, error) {} +func (noopMeter) KMSSigned(error) {} + +type instrumentedKeyManager struct { + kms.KeyManager + meter Meter +} + +func (i *instrumentedKeyManager) CreateSigner(req *kmsapi.CreateSignerRequest) (s crypto.Signer, err error) { + if s, err = i.KeyManager.CreateSigner(req); err == nil { + s = &instrumentedKMSSigner{s, i.meter} + } + + return +} + +type instrumentedKMSSigner struct { + crypto.Signer + meter Meter +} + +func (i *instrumentedKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + signature, err = i.Signer.Sign(rand, digest, opts) + i.meter.KMSSigned(err) + + return +} diff --git a/authority/options.go b/authority/options.go index 4fc5a20f..82c62bc4 100644 --- a/authority/options.go +++ b/authority/options.go @@ -381,3 +381,16 @@ func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) { } return certs, nil } + +// WithMeter is an option that sets the authority's [Meter] to the provided one. +func WithMeter(m Meter) Option { + if m == nil { + m = noopMeter{} + } + + return func(a *Authority) (_ error) { + a.meter = m + + return + } +} diff --git a/authority/ssh.go b/authority/ssh.go index f9371d60..756e376e 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -146,7 +146,13 @@ func (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (* } // SignSSH creates a signed SSH certificate with the given public key and options. -func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { +func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { + cert, prov, err := a.signSSH(ctx, key, opts, signOpts...) + a.meter.SSHSigned(prov, err) + return cert, err +} + +func (a *Authority) signSSH(_ context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) { var ( certOptions []sshutil.Option mods []provisioner.SSHCertModifier @@ -155,7 +161,7 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision // Validate given options. if err := opts.Validate(); err != nil { - return nil, err + return nil, nil, err } // Set backdate with the configured value @@ -184,7 +190,7 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision // validate the given SSHOptions case provisioner.SSHCertOptionsValidator: if err := o.Valid(opts); err != nil { - return nil, errs.BadRequestErr(err, "error validating ssh certificate options") + return nil, prov, errs.BadRequestErr(err, "error validating ssh certificate options") } // call webhooks @@ -192,7 +198,7 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision webhookCtl = o default: - return nil, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o) + return nil, prov, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o) } } @@ -205,8 +211,8 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision } // Call enriching webhooks - if err := callEnrichingWebhooksSSH(webhookCtl, cr); err != nil { - return nil, errs.ApplyOptions( + if err := a.callEnrichingWebhooksSSH(prov, webhookCtl, cr); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, err.Error()), errs.WithKeyVal("signOptions", signOpts), ) @@ -216,20 +222,21 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision certificate, err := sshutil.NewCertificate(cr, certOptions...) if err != nil { var te *sshutil.TemplateError - if errors.As(err, &te) { - return nil, errs.ApplyOptions( + switch { + case errors.As(err, &te): + return nil, prov, errs.ApplyOptions( errs.BadRequestErr(err, err.Error()), errs.WithKeyVal("signOptions", signOpts), ) - } - // explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors - if strings.HasPrefix(err.Error(), "error unmarshaling certificate") { - return nil, errs.InternalServerErr(templatingError(err), + case strings.HasPrefix(err.Error(), "error unmarshaling certificate"): + // explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors + return nil, prov, errs.InternalServerErr(templatingError(err), errs.WithKeyVal("signOptions", signOpts), errs.WithMessage("error applying certificate template"), ) + default: + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH") } - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH") } // Get actual *ssh.Certificate and continue with provisioner modifiers. @@ -238,13 +245,13 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision // Use SignSSHOptions to modify the certificate validity. It will be later // checked or set if not defined. if err := opts.ModifyValidity(certTpl); err != nil { - return nil, errs.BadRequestErr(err, err.Error()) + return nil, prov, errs.BadRequestErr(err, err.Error()) } // Use provisioner modifiers. for _, m := range mods { if err := m.Modify(certTpl, opts); err != nil { - return nil, errs.ForbiddenErr(err, "error creating ssh certificate") + return nil, prov, errs.ForbiddenErr(err, "error creating ssh certificate") } } @@ -253,32 +260,32 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision switch certTpl.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented("authority.SignSSH: user certificate signing is not enabled") + return nil, prov, errs.NotImplemented("authority.SignSSH: user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented("authority.SignSSH: host certificate signing is not enabled") + return nil, prov, errs.NotImplemented("authority.SignSSH: host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) + return nil, prov, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType) } // Check if authority is allowed to sign the certificate if err := a.isAllowedToSignSSHCertificate(certTpl); err != nil { var ee *errs.Error if errors.As(err, &ee) { - return nil, ee + return nil, prov, ee } - return nil, errs.InternalServerErr(err, + return nil, prov, errs.InternalServerErr(err, errs.WithMessage("authority.SignSSH: error creating ssh certificate"), ) } // Send certificate to webhooks for authorization - if err := callAuthorizingWebhooksSSH(webhookCtl, certificate, certTpl); err != nil { - return nil, errs.ApplyOptions( + if err := a.callAuthorizingWebhooksSSH(prov, webhookCtl, certificate, certTpl); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "authority.SignSSH: error signing certificate"), ) } @@ -286,21 +293,21 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision // Sign certificate. cert, err := sshutil.CreateCertificate(certTpl, signer) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error signing certificate") + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error signing certificate") } // User provisioners validators. for _, v := range validators { if err := v.Valid(cert, opts); err != nil { - return nil, errs.ForbiddenErr(err, "error validating ssh certificate") + return nil, prov, errs.ForbiddenErr(err, "error validating ssh certificate") } } - if err = a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db") + if err := a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db") } - return cert, nil + return cert, prov, nil } // isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate. @@ -310,12 +317,18 @@ func (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error { // RenewSSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) { + cert, prov, err := a.renewSSH(ctx, oldCert) + a.meter.SSHRenewed(prov, err) + return cert, err +} + +func (a *Authority) renewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, provisioner.Interface, error) { if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errs.BadRequest("cannot renew a certificate without validity period") + return nil, nil, errs.BadRequest("cannot renew a certificate without validity period") } if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil { - return nil, err + return nil, nil, err } // Attempt to extract the provisioner from the token. @@ -348,36 +361,41 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss switch certTpl.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented("renewSSH: user certificate signing is not enabled") + return nil, prov, errs.NotImplemented("renewSSH: user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented("renewSSH: host certificate signing is not enabled") + return nil, prov, errs.NotImplemented("renewSSH: host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", certTpl.CertType) + return nil, prov, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", certTpl.CertType) } // Sign certificate. cert, err := sshutil.CreateCertificate(certTpl, signer) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } - if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { - return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") + if err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") } - return cert, nil + return cert, prov, nil } // RekeySSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { - var validators []provisioner.SSHCertValidator + cert, prov, err := a.rekeySSH(ctx, oldCert, pub, signOpts...) + a.meter.SSHRekeyed(prov, err) + return cert, err +} +func (a *Authority) rekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) { var prov provisioner.Interface + var validators []provisioner.SSHCertValidator for _, op := range signOpts { switch o := op.(type) { // Capture current provisioner @@ -387,16 +405,16 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub case provisioner.SSHCertValidator: validators = append(validators, o) default: - return nil, errs.InternalServer("rekeySSH; invalid extra option type %T", o) + return nil, prov, errs.InternalServer("rekeySSH; invalid extra option type %T", o) } } if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errs.BadRequest("cannot rekey a certificate without validity period") + return nil, prov, errs.BadRequest("cannot rekey a certificate without validity period") } if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil { - return nil, err + return nil, prov, err } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -423,37 +441,37 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub switch cert.CertType { case ssh.UserCert: if a.sshCAUserCertSignKey == nil { - return nil, errs.NotImplemented("rekeySSH; user certificate signing is not enabled") + return nil, prov, errs.NotImplemented("rekeySSH; user certificate signing is not enabled") } signer = a.sshCAUserCertSignKey case ssh.HostCert: if a.sshCAHostCertSignKey == nil { - return nil, errs.NotImplemented("rekeySSH; host certificate signing is not enabled") + return nil, prov, errs.NotImplemented("rekeySSH; host certificate signing is not enabled") } signer = a.sshCAHostCertSignKey default: - return nil, errs.BadRequest("unexpected certificate type '%d'", cert.CertType) + return nil, prov, errs.BadRequest("unexpected certificate type '%d'", cert.CertType) } var err error // Sign certificate. cert, err = sshutil.CreateCertificate(cert, signer) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } // Apply validators from provisioner. for _, v := range validators { if err := v.Valid(cert, provisioner.SignSSHOptions{Backdate: backdate}); err != nil { - return nil, errs.ForbiddenErr(err, "error validating ssh certificate") + return nil, prov, errs.ForbiddenErr(err, "error validating ssh certificate") } } - if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { - return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") + if err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) { + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") } - return cert, nil + return cert, prov, nil } func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error { @@ -653,28 +671,36 @@ func (a *Authority) getAddUserCommand(principal string) string { return strings.ReplaceAll(cmd, "", principal) } -func callEnrichingWebhooksSSH(webhookCtl webhookController, cr sshutil.CertificateRequest) error { +func (a *Authority) callEnrichingWebhooksSSH(prov provisioner.Interface, webhookCtl webhookController, cr sshutil.CertificateRequest) (err error) { if webhookCtl == nil { - return nil + return } - whEnrichReq, err := webhook.NewRequestBody( + + var whEnrichReq *webhook.RequestBody + if whEnrichReq, err = webhook.NewRequestBody( webhook.WithSSHCertificateRequest(cr), - ) - if err != nil { - return err + ); err == nil { + err = webhookCtl.Enrich(whEnrichReq) + + a.meter.SSHWebhookEnriched(prov, err) } - return webhookCtl.Enrich(whEnrichReq) + + return } -func callAuthorizingWebhooksSSH(webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) error { +func (a *Authority) callAuthorizingWebhooksSSH(prov provisioner.Interface, webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) (err error) { if webhookCtl == nil { - return nil + return } - whAuthBody, err := webhook.NewRequestBody( + + var whAuthBody *webhook.RequestBody + if whAuthBody, err = webhook.NewRequestBody( webhook.WithSSHCertificate(cert, certTpl), - ) - if err != nil { - return err + ); err == nil { + err = webhookCtl.Authorize(whAuthBody) + + a.meter.SSHWebhookAuthorized(prov, err) } - return webhookCtl.Authorize(whAuthBody) + + return } diff --git a/authority/tls.go b/authority/tls.go index 7da8ec40..0dd6eb54 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -93,6 +93,12 @@ func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc { // Sign creates a signed certificate from a certificate signing request. func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { + chain, prov, err := a.signX509(csr, signOpts, extraOpts...) + a.meter.X509Signed(prov, err) + return chain, err +} + +func (a *Authority) signX509(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, provisioner.Interface, error) { var ( certOptions []x509util.Option certValidators []provisioner.CertificateValidator @@ -100,9 +106,9 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign certEnforcers []provisioner.CertificateEnforcer ) - opts := []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} + opts := []any{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)} if err := csr.CheckSignature(); err != nil { - return nil, errs.ApplyOptions( + return nil, nil, errs.ApplyOptions( errs.BadRequestErr(err, "invalid certificate request"), opts..., ) @@ -111,10 +117,12 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Set backdate with the configured value signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration - var prov provisioner.Interface - var pInfo *casapi.ProvisionerInfo - var attData *provisioner.AttestationData - var webhookCtl webhookController + var ( + prov provisioner.Interface + pInfo *casapi.ProvisionerInfo + attData *provisioner.AttestationData + webhookCtl webhookController + ) for _, op := range extraOpts { switch k := op.(type) { // Capture current provisioner @@ -132,7 +140,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Validate the given certificate request. case provisioner.CertificateRequestValidator: if err := k.Valid(csr); err != nil { - return nil, errs.ApplyOptions( + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error validating certificate request"), opts..., ) @@ -159,45 +167,46 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign webhookCtl = k default: - return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...) + return nil, prov, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]any{k}, opts...)...) } } - if err := callEnrichingWebhooksX509(webhookCtl, attData, csr); err != nil { - return nil, errs.ApplyOptions( + if err := a.callEnrichingWebhooksX509(prov, webhookCtl, attData, csr); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, err.Error()), errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts), ) } - cert, err := x509util.NewCertificate(csr, certOptions...) + crt, err := x509util.NewCertificate(csr, certOptions...) if err != nil { var te *x509util.TemplateError - if errors.As(err, &te) { - return nil, errs.ApplyOptions( + switch { + case errors.As(err, &te): + return nil, prov, errs.ApplyOptions( errs.BadRequestErr(err, err.Error()), errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts), ) - } - // explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors - if strings.HasPrefix(err.Error(), "error unmarshaling certificate") { - return nil, errs.InternalServerErr(templatingError(err), + case strings.HasPrefix(err.Error(), "error unmarshaling certificate"): + // explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors + return nil, prov, errs.InternalServerErr(templatingError(err), errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts), errs.WithMessage("error applying certificate template"), ) + default: + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...) } - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...) } // Certificate modifiers before validation - leaf := cert.GetCertificate() + leaf := crt.GetCertificate() // Set default subject if err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil { - return nil, errs.ApplyOptions( + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error creating certificate"), opts..., ) @@ -205,7 +214,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign for _, m := range certModifiers { if err := m.Modify(leaf, signOpts); err != nil { - return nil, errs.ApplyOptions( + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error creating certificate"), opts..., ) @@ -215,7 +224,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Certificate validation. for _, v := range certValidators { if err := v.Valid(leaf, signOpts); err != nil { - return nil, errs.ApplyOptions( + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error validating certificate"), opts..., ) @@ -224,8 +233,8 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Certificate modifiers after validation for _, m := range certEnforcers { - if err := m.Enforce(leaf); err != nil { - return nil, errs.ApplyOptions( + if err = m.Enforce(leaf); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error creating certificate"), opts..., ) @@ -234,8 +243,8 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Process injected modifiers after validation for _, m := range a.x509Enforcers { - if err := m.Enforce(leaf); err != nil { - return nil, errs.ApplyOptions( + if err = m.Enforce(leaf); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error creating certificate"), opts..., ) @@ -243,12 +252,12 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } // Check if authority is allowed to sign the certificate - if err := a.isAllowedToSignX509Certificate(leaf); err != nil { + if err = a.isAllowedToSignX509Certificate(leaf); err != nil { var ee *errs.Error if errors.As(err, &ee) { - return nil, errs.ApplyOptions(ee, opts...) + return nil, prov, errs.ApplyOptions(ee, opts...) } - return nil, errs.InternalServerErr(err, + return nil, prov, errs.InternalServerErr(err, errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts), errs.WithMessage("error creating certificate"), @@ -256,8 +265,8 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign } // Send certificate to webhooks for authorization - if err := callAuthorizingWebhooksX509(webhookCtl, cert, leaf, attData); err != nil { - return nil, errs.ApplyOptions( + if err := a.callAuthorizingWebhooksX509(prov, webhookCtl, crt, leaf, attData); err != nil { + return nil, prov, errs.ApplyOptions( errs.ForbiddenErr(err, "error creating certificate"), opts..., ) @@ -265,6 +274,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign // Sign certificate lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) + resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ Template: leaf, CSR: csr, @@ -273,23 +283,22 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign Provisioner: pInfo, }) if err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...) + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...) } - fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) + chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) - // Wrap provisioner with extra information. - prov = wrapProvisioner(prov, attData) + // Wrap provisioner with extra information, if not nil + if prov != nil { + prov = wrapProvisioner(prov, attData) + } // Store certificate in the db. - if err = a.storeCertificate(prov, fullchain); err != nil { - if !errors.Is(err, db.ErrNotImplemented) { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "authority.Sign; error storing certificate in db", opts...) - } + if err := a.storeCertificate(prov, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) { + return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...) } - return fullchain, nil + return chain, prov, nil } // isAllowedToSignX509Certificate checks if the Authority is allowed @@ -337,14 +346,25 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // of rekey), and 'NotBefore/NotAfter' (the validity duration of the new // certificate should be equal to the old one, but starting 'now'). func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { + chain, prov, err := a.renewContext(ctx, oldCert, pk) + if pk == nil { + a.meter.X509Renewed(prov, err) + } else { + a.meter.X509Rekeyed(prov, err) + } + return chain, err +} + +func (a *Authority) renewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, provisioner.Interface, error) { isRekey := (pk != nil) opts := []errs.Option{ errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()), } // Check step provisioner extensions - if err := a.authorizeRenew(ctx, oldCert); err != nil { - return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) + prov, err := a.authorizeRenew(ctx, oldCert) + if err != nil { + return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) } // Durations @@ -414,15 +434,17 @@ func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, // // TODO(hslatman,maraino): consider adding policies too and consider if // RenewSSH should check policies. - if err := a.constraintsEngine.ValidateCertificate(newCert); err != nil { + if err = a.constraintsEngine.ValidateCertificate(newCert); err != nil { var ee *errs.Error - if errors.As(err, &ee) { - return nil, errs.StatusCodeError(ee.StatusCode(), err, opts...) + switch { + case errors.As(err, &ee): + return nil, prov, errs.StatusCodeError(ee.StatusCode(), err, opts...) + default: + return nil, prov, errs.InternalServerErr(err, + errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()), + errs.WithMessage("error renewing certificate"), + ) } - return nil, errs.InternalServerErr(err, - errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()), - errs.WithMessage("error renewing certificate"), - ) } // The token can optionally be in the context. If the CA is running in RA @@ -436,17 +458,16 @@ func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, Token: token, }) if err != nil { - return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) + return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) } - fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) - if err = a.storeRenewedCertificate(oldCert, fullchain); err != nil { - if !errors.Is(err, db.ErrNotImplemented) { - return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) - } + chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...) + + if err = a.storeRenewedCertificate(oldCert, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) { + return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...) } - return fullchain, nil + return chain, prov, nil } // storeCertificate allows to use an extension of the db.AuthDB interface that @@ -952,42 +973,52 @@ func templatingError(err error) error { return errors.Wrap(cause, "error applying certificate template") } -func callEnrichingWebhooksX509(webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) error { +func (a *Authority) callEnrichingWebhooksX509(prov provisioner.Interface, webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) (err error) { if webhookCtl == nil { - return nil + return } + var attested *webhook.AttestationData if attData != nil { attested = &webhook.AttestationData{ PermanentIdentifier: attData.PermanentIdentifier, } } - whEnrichReq, err := webhook.NewRequestBody( + + var whEnrichReq *webhook.RequestBody + if whEnrichReq, err = webhook.NewRequestBody( webhook.WithX509CertificateRequest(csr), webhook.WithAttestationData(attested), - ) - if err != nil { - return err + ); err == nil { + err = webhookCtl.Enrich(whEnrichReq) + + a.meter.X509WebhookEnriched(prov, err) } - return webhookCtl.Enrich(whEnrichReq) + + return } -func callAuthorizingWebhooksX509(webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) error { +func (a *Authority) callAuthorizingWebhooksX509(prov provisioner.Interface, webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) (err error) { if webhookCtl == nil { - return nil + return } + var attested *webhook.AttestationData if attData != nil { attested = &webhook.AttestationData{ PermanentIdentifier: attData.PermanentIdentifier, } } - whAuthBody, err := webhook.NewRequestBody( + + var whAuthBody *webhook.RequestBody + if whAuthBody, err = webhook.NewRequestBody( webhook.WithX509Certificate(cert, leaf), webhook.WithAttestationData(attested), - ) - if err != nil { - return err + ); err == nil { + err = webhookCtl.Authorize(whAuthBody) + + a.meter.X509WebhookAuthorized(prov, err) } - return webhookCtl.Authorize(whAuthBody) + + return } diff --git a/ca/ca.go b/ca/ca.go index 7baf2419..0059a5d0 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -27,6 +27,7 @@ import ( adminAPI "github.com/smallstep/certificates/authority/admin/api" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/internal/metrix" "github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/monitoring" "github.com/smallstep/certificates/scep" @@ -125,6 +126,7 @@ type CA struct { config *config.Config srv *server.Server insecureSrv *server.Server + metricsSrv *server.Server opts *options renewer *TLSRenewer compactStop chan struct{} @@ -163,6 +165,13 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { opts = append(opts, authority.WithQuietInit()) } + var meter *metrix.Meter + if ca.config.MetricsAddress != "" { + meter = metrix.New() + + opts = append(opts, authority.WithMeter(meter)) + } + webhookTransport := http.DefaultTransport.(*http.Transport).Clone() opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport})) @@ -318,6 +327,13 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { } } + if meter != nil { + ca.metricsSrv = server.New(ca.config.MetricsAddress, meter, nil) + ca.metricsSrv.BaseContext = func(net.Listener) context.Context { + return baseContext + } + } + return ca, nil } @@ -404,6 +420,14 @@ func (ca *CA) Run() error { }() } + if ca.metricsSrv != nil { + wg.Add(1) + go func() { + defer wg.Done() + errs <- ca.metricsSrv.ListenAndServe() + }() + } + wg.Add(1) go func() { defer wg.Done() @@ -480,6 +504,13 @@ func (ca *CA) Reload() error { } } + if ca.metricsSrv != nil { + if err = ca.metricsSrv.Reload(newCA.metricsSrv); err != nil { + logContinue("Reload failed because metrics server could not be replaced.") + return errors.Wrap(err, "error reloading metrics server") + } + } + if err = ca.srv.Reload(newCA.srv); err != nil { logContinue("Reload failed because server could not be replaced.") return errors.Wrap(err, "error reloading server") diff --git a/commands/app.go b/commands/app.go index e5c6ea1e..c96b50ae 100644 --- a/commands/app.go +++ b/commands/app.go @@ -251,7 +251,8 @@ To get a linked authority token: ca.WithSSHUserPassword(sshUserPassword), ca.WithIssuerPassword(issuerPassword), ca.WithLinkedCAToken(token), - ca.WithQuiet(quiet)) + ca.WithQuiet(quiet), + ) if err != nil { fatal(err) } diff --git a/go.mod b/go.mod index 31911b98..6d23bdac 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/hashicorp/vault/api/auth/kubernetes v0.5.0 github.com/newrelic/go-agent/v3 v3.29.0 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.15.1 github.com/rs/xid v1.5.0 github.com/sirupsen/logrus v1.9.3 github.com/slackhq/nebula v1.6.1 @@ -73,6 +74,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.19.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -125,6 +127,7 @@ require ( github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -134,6 +137,9 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/schollz/jsonstore v1.1.0 // indirect diff --git a/go.sum b/go.sum index e74913d9..7ba1523d 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGz github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -350,6 +352,8 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -383,7 +387,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -540,6 +552,7 @@ golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -653,8 +666,8 @@ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7 google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/metrix/meter.go b/internal/metrix/meter.go new file mode 100644 index 00000000..a867b197 --- /dev/null +++ b/internal/metrix/meter.go @@ -0,0 +1,196 @@ +// Package metrix implements stats-related functionality. +package metrix + +import ( + "net/http" + "strconv" + "time" + + "github.com/smallstep/certificates/authority/provisioner" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// New initializes and returns a new [Meter]. +func New() (m *Meter) { + initializedAt := time.Now() + + m = &Meter{ + uptime: prometheus.NewGaugeFunc( + prometheus.GaugeOpts(opts( + "", + "uptime_seconds", + "Number of seconds since service start", + )), + func() float64 { + return float64(time.Since(initializedAt) / time.Second) + }, + ), + ssh: newProvisionerInstruments("ssh"), + x509: newProvisionerInstruments("x509"), + kms: &kms{ + signed: prometheus.NewCounter(prometheus.CounterOpts(opts("kms", "signed", "Number of KMS-backed signatures"))), + errors: prometheus.NewCounter(prometheus.CounterOpts(opts("kms", "errors", "Number of KMS-related errors"))), + }, + } + + reg := prometheus.NewRegistry() + + reg.MustRegister( + m.uptime, + m.ssh.rekeyed, + m.ssh.renewed, + m.ssh.signed, + m.x509.rekeyed, + m.x509.renewed, + m.x509.signed, + m.kms.signed, + m.kms.errors, + ) + + h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{ + Registry: reg, + Timeout: 5 * time.Second, + MaxRequestsInFlight: 10, + }) + + mux := http.NewServeMux() + mux.Handle("/metrics", h) + m.Handler = mux + + return +} + +// Meter wraps the functionality of a Prometheus-compatible HTTP handler. +type Meter struct { + http.Handler + + uptime prometheus.GaugeFunc + ssh *provisionerInstruments + x509 *provisionerInstruments + kms *kms +} + +// SSHRekeyed implements [authority.Meter] for [Meter]. +func (m *Meter) SSHRekeyed(p provisioner.Interface, err error) { + incrProvisionerCounter(m.ssh.rekeyed, p, err) +} + +// SSHRenewed implements [authority.Meter] for [Meter]. +func (m *Meter) SSHRenewed(p provisioner.Interface, err error) { + incrProvisionerCounter(m.ssh.renewed, p, err) +} + +// SSHSigned implements [authority.Meter] for [Meter]. +func (m *Meter) SSHSigned(p provisioner.Interface, err error) { + incrProvisionerCounter(m.ssh.signed, p, err) +} + +// SSHAuthorized implements [authority.Meter] for [Meter]. +func (m *Meter) SSHWebhookAuthorized(p provisioner.Interface, err error) { + incrProvisionerCounter(m.ssh.webhookAuthorized, p, err) +} + +// SSHEnriched implements [authority.Meter] for [Meter]. +func (m *Meter) SSHWebhookEnriched(p provisioner.Interface, err error) { + incrProvisionerCounter(m.ssh.webhookEnriched, p, err) +} + +// X509Rekeyed implements [authority.Meter] for [Meter]. +func (m *Meter) X509Rekeyed(p provisioner.Interface, err error) { + incrProvisionerCounter(m.x509.rekeyed, p, err) +} + +// X509Renewed implements [authority.Meter] for [Meter]. +func (m *Meter) X509Renewed(p provisioner.Interface, err error) { + incrProvisionerCounter(m.x509.renewed, p, err) +} + +// X509Signed implements [authority.Meter] for [Meter]. +func (m *Meter) X509Signed(p provisioner.Interface, err error) { + incrProvisionerCounter(m.x509.signed, p, err) +} + +// X509Authorized implements [authority.Meter] for [Meter]. +func (m *Meter) X509WebhookAuthorized(p provisioner.Interface, err error) { + incrProvisionerCounter(m.x509.webhookAuthorized, p, err) +} + +// X509Enriched implements [authority.Meter] for [Meter]. +func (m *Meter) X509WebhookEnriched(p provisioner.Interface, err error) { + incrProvisionerCounter(m.x509.webhookEnriched, p, err) +} + +func incrProvisionerCounter(cv *prometheus.CounterVec, p provisioner.Interface, err error) { + var name string + if p != nil { + name = p.GetName() + } + + cv.WithLabelValues(name, strconv.FormatBool(err == nil)).Inc() +} + +// KMSSigned implements [authority.Meter] for [Meter]. +func (m *Meter) KMSSigned(err error) { + if err == nil { + m.kms.signed.Inc() + } else { + m.kms.errors.Inc() + } +} + +// provisionerInstruments wraps the counters exported by provisioners. +type provisionerInstruments struct { + rekeyed *prometheus.CounterVec + renewed *prometheus.CounterVec + signed *prometheus.CounterVec + + webhookAuthorized *prometheus.CounterVec + webhookEnriched *prometheus.CounterVec +} + +func newProvisionerInstruments(subsystem string) *provisionerInstruments { + return &provisionerInstruments{ + rekeyed: newCounterVec(subsystem, "rekeyed_total", "Number of certificates rekeyed", + "provisioner", + "success", + ), + renewed: newCounterVec(subsystem, "renewed_total", "Number of certificates renewed", + "provisioner", + "success", + ), + signed: newCounterVec(subsystem, "signed_total", "Number of certificates signed", + "provisioner", + "success", + ), + webhookAuthorized: newCounterVec(subsystem, "webhook_authorized_total", "Number of authorizing webhooks called", + "provisioner", + "success", + ), + webhookEnriched: newCounterVec(subsystem, "webhook_enriched_total", "Number of enriching webhooks called", + "provisioner", + "success", + ), + } +} + +type kms struct { + signed prometheus.Counter + errors prometheus.Counter +} + +func newCounterVec(subsystem, name, help string, labels ...string) *prometheus.CounterVec { + opts := opts(subsystem, name, help) + + return prometheus.NewCounterVec(prometheus.CounterOpts(opts), labels) +} + +func opts(subsystem, name, help string) prometheus.Opts { + return prometheus.Opts{ + Namespace: "step_ca", + Subsystem: subsystem, + Name: name, + Help: help, + } +}