Repository: scionproto/scion
Pull request #3819 (2020-07-03 13:58:24)

cs: add metrics for signer and ca signer
Add additional metrics that expose the state of the signer and CA signer
in the control service. Both signers are cached for a certain amount
of time. The metrics expose the last time each signer was successfully
generated.

BREAKING CHANGE: renaming one metric, see below.

As the CA signer can be unavailable, i.e., if the key is removed from
disk, the metrics expose whether it is active or not.

The new metrics are:

    # HELP trustengine_ca_signer_active_boolean Wether the CA signer is active and can sign certificate chains
    # TYPE trustengine_ca_signer_active_boolean gauge
    trustengine_ca_signer_active_boolean 1
    # HELP trustengine_ca_signer_expiration_time_second The expiration time of the current CA signer
    # TYPE trustengine_ca_signer_expiration_time_second gauge
    trustengine_ca_signer_expiration_time_second 1.655025547e+09
    # HELP trustengine_generated_ca_signers_total Number of generated CA signers that sign certificate chains
    # TYPE trustengine_generated_ca_signers_total counter
    trustengine_generated_ca_signers_total{result="ok_success"} 1
    # HELP trustengine_generated_signers_total Number of generated signers backed by the trust engine
    # TYPE trustengine_generated_signers_total counter
    trustengine_generated_signers_total{result="ok_success"} 103
    # HELP trustengine_last_ca_signer_generation_time_second The last time a signer for creating AS certificates was successfully generated
    # TYPE trustengine_last_ca_signer_generation_time_second gauge
    trustengine_last_ca_signer_generation_time_second 1.5919535885081732e+09
    # HELP trustengine_signed_certificate_chains_total Number of certificate chains signed
    # Type trustengine_signed_certificate_chains_total counter
    trustengine_signed_certificate_chains_total{result="ok_success"} 3
    # HELP trustengine_last_signer_generation_time_second The last time a signer for control plane messages was successfully generated
    # TYPE trustengine_last_signer_generation_time_second gauge
    trustengine_last_signer_generation_time_second 1.5919541586176305e+09
    # HELP trustengine_signer_expiration_time_second The expiration time of the current signer
    # TYPE trustengine_signer_expiration_time_second gauge
    trustengine_signer_expiration_time_second 1.592212788e+09

The metric:

    trustengine_signer_expiration_time_second

replaces:

    trustengine_latest_generated_signer_expiration_time_second

Additionally, a new HTTP endpoint is exposed on `<metrics-address>/ca`:

    {
        "subject": {
            "isd_as": "1-ff00:0:110"
        },
        "subject_key_id": "B3 17 35 20 BB 92 A1 C7 91 87 90 15 FE DC 3D E2 CE FB 78 23",
        "policy": {
            "chain_lifetime": "72h0m0s"
        },
        "cert_validity": {
            "not_before": "2020-06-12T09:19:07Z",
            "not_after": "2022-06-12T09:19:07Z"
        }
    }

Additionally, add a section to the control servers config file to make the AS
certificate validity configurable.

New section is:

```
[ca]
    # The maximum validity time of a renewed AS certificate the control server
    # creates in a CA AS. The remaining validity of the locally available CA
    # certificate must be larger than the here configured value at every given point
    # in time. (i.e., ca.not_after - current_time >= max_as_validity) If that is not
    # the case, certificate renewal is not possible until a new CA certificate is
    # loaded that satisfies the condition. (default 3d)
    max_as_validity = "3d"
```

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/scionproto/scion/3819)
<!-- Reviewable:end -->



Patch:
diff --git a/go/cs/config/config.go b/go/cs/config/config.go
index 4d00b80f7..0615e5935 100644
--- a/go/cs/config/config.go
+++ b/go/cs/config/config.go
@@ -56,6 +56,8 @@ const (
 	// DefaultQueryInterval is the default interval after which the segment
 	// cache expires.
 	DefaultQueryInterval = 5 * time.Minute
+	// DefaultMaxASValidity is the default validity period for renewed AS certificates.
+	DefaultMaxASValidity = 3 * 24 * time.Hour
 )
 
 // Error values
@@ -80,6 +82,7 @@ type Config struct {
 	PathDB    pathstorage.PathDBConf     `toml:"path_db,omitempty"`
 	BS        BSConfig                   `toml:"beaconing,omitempty"`
 	PS        PSConfig                   `toml:"path,omitempty"`
+	CA        CA                         `toml:"ca,omitempty"`
 }
 
 // InitDefaults initializes the default values for all parts of the config.
@@ -96,6 +99,7 @@ func (cfg *Config) InitDefaults() {
 		&cfg.PathDB,
 		&cfg.BS,
 		&cfg.PS,
+		&cfg.CA,
 	)
 }
 
@@ -112,6 +116,7 @@ func (cfg *Config) Validate() error {
 		&cfg.PathDB,
 		&cfg.BS,
 		&cfg.PS,
+		&cfg.CA,
 	)
 }
 
@@ -130,6 +135,7 @@ func (cfg *Config) Sample(dst io.Writer, path config.Path, _ config.CtxMap) {
 		&cfg.PathDB,
 		&cfg.BS,
 		&cfg.PS,
+		&cfg.CA,
 	)
 }
 
@@ -282,3 +288,25 @@ func (cfg *Policies) Sample(dst io.Writer, _ config.Path, _ config.CtxMap) {
 func (cfg *Policies) ConfigName() string {
 	return "policies"
 }
+
+// CA is the CA configuration.
+type CA struct {
+	config.NoDefaulter
+	// MaxASValidity is the maximum AS certificate lifetime.
+	MaxASValidity util.DurWrap `toml:"max_as_validity,omitempty"`
+}
+
+func (cfg *CA) Validate() error {
+	if cfg.MaxASValidity.Duration == 0 {
+		cfg.MaxASValidity.Duration = DefaultMaxASValidity
+	}
+	return nil
+}
+
+func (cfg *CA) Sample(dst io.Writer, _ config.Path, _ config.CtxMap) {
+	config.WriteString(dst, caSample)
+}
+
+func (cfg *CA) ConfigName() string {
+	return "ca"
+}
diff --git a/go/cs/config/sample.go b/go/cs/config/sample.go
index f1a313fab..57d96389d 100644
--- a/go/cs/config/sample.go
+++ b/go/cs/config/sample.go
@@ -20,3 +20,13 @@ const psSample = `
 # The time after which segments for a destination are refetched. (default 5m)
 query_interval = "5m"
 `
+
+const caSample = `
+# The maximum validity time of a renewed AS certificate the control server
+# creates in a CA AS. The remaining validity of the locally available CA
+# certificate must be larger than the here configured value at every given point
+# in time. (i.e., ca.not_after - current_time >= max_as_validity) If that is not
+# the case, certificate renewal is not possible until a new CA certificate is
+# loaded that satisfies the condition. (default 3d)
+max_as_validity = "3d"
+`
diff --git a/go/pkg/cs/BUILD.bazel b/go/pkg/cs/BUILD.bazel
index 9e1042cd4..ec401d2ca 100644
--- a/go/pkg/cs/BUILD.bazel
+++ b/go/pkg/cs/BUILD.bazel
@@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
 
 go_library(
     name = "go_default_library",
-    srcs = ["main.go"],
+    srcs = [
+        "http.go",
+        "main.go",
+    ],
     importpath = "github.com/scionproto/scion/go/pkg/cs",
     visibility = ["//visibility:public"],
     deps = [
@@ -41,6 +44,7 @@ go_library(
         "//go/lib/prom:go_default_library",
         "//go/lib/revcache:go_default_library",
         "//go/lib/scrypto:go_default_library",
+        "//go/lib/scrypto/cppki:go_default_library",
         "//go/lib/serrors:go_default_library",
         "//go/lib/snet:go_default_library",
         "//go/lib/snet/addrutil:go_default_library",
diff --git a/go/pkg/cs/http.go b/go/pkg/cs/http.go
new file mode 100644
index 000000000..9e17b5d3d
--- /dev/null
+++ b/go/pkg/cs/http.go
@@ -0,0 +1,129 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cs
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/scionproto/scion/go/lib/addr"
+	"github.com/scionproto/scion/go/lib/scrypto"
+	"github.com/scionproto/scion/go/lib/scrypto/cppki"
+	cstrust "github.com/scionproto/scion/go/pkg/cs/trust"
+)
+
+func signerHandler(signer cstrust.RenewingSigner) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		s, err := signer.SignerGen.Generate(r.Context())
+		if err != nil {
+			http.Error(w, "Unable to get signer", http.StatusInternalServerError)
+			return
+		}
+
+		type Subject struct {
+			IA addr.IA `json:"isd_as"`
+		}
+		type TRCID struct {
+			ISD    addr.ISD        `json:"isd"`
+			Base   scrypto.Version `json:"base_number"`
+			Serial scrypto.Version `json:"serial_number"`
+		}
+		type Validity struct {
+			NotBefore time.Time `json:"not_before"`
+			NotAfter  time.Time `json:"not_after"`
+		}
+		rep := struct {
+			Subject       Subject   `json:"subject"`
+			SubjectKeyID  string    `json:"subject_key_id"`
+			Expiration    time.Time `json:"expiration"`
+			TRCID         TRCID     `json:"trc_id"`
+			ChainValidity Validity  `json:"chain_validity"`
+			InGrace       bool      `json:"in_grace_period"`
+		}{
+			Subject:      Subject{IA: s.IA},
+			SubjectKeyID: fmt.Sprintf("% X", s.SubjectKeyID),
+			Expiration:   s.Expiration,
+			TRCID: TRCID{
+				ISD:    s.TRCID.ISD,
+				Base:   s.TRCID.Base,
+				Serial: s.TRCID.Serial,
+			},
+			ChainValidity: Validity{
+				NotBefore: s.ChainValidity.NotBefore,
+				NotAfter:  s.ChainValidity.NotAfter,
+			},
+			InGrace: s.InGrace,
+		}
+		enc := json.NewEncoder(w)
+		enc.SetIndent("", "    ")
+		if err := enc.Encode(rep); err != nil {
+			http.Error(w, "Unable to marshal response", http.StatusInternalServerError)
+			return
+		}
+	}
+}
+
+func caHandler(signer cstrust.ChainBuilder) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		s, err := signer.PolicyGen.Generate(r.Context())
+		if err != nil {
+			http.Error(w, "No active signer", http.StatusInternalServerError)
+			return
+		}
+
+		ia, err := cppki.ExtractIA(s.Certificate.Subject)
+		if err != nil || ia == nil {
+			http.Error(w, "Unable to get extract ISD-AS", http.StatusInternalServerError)
+			return
+		}
+
+		type Subject struct {
+			IA addr.IA `json:"isd_as"`
+		}
+		type Validity struct {
+			NotBefore time.Time `json:"not_before"`
+			NotAfter  time.Time `json:"not_after"`
+		}
+		type Policy struct {
+			ChainLifetime string `json:"chain_lifetime"`
+		}
+		rep := struct {
+			Subject      Subject  `json:"subject"`
+			SubjectKeyID string   `json:"subject_key_id"`
+			Policy       Policy   `json:"policy"`
+			CertValidity Validity `json:"cert_validity"`
+		}{
+			Subject:      Subject{IA: *ia},
+			SubjectKeyID: fmt.Sprintf("% X", s.Certificate.SubjectKeyId),
+			Policy: Policy{
+				ChainLifetime: fmt.Sprintf("%s", s.Validity),
+			},
+			CertValidity: Validity{
+				NotBefore: s.Certificate.NotBefore,
+				NotAfter:  s.Certificate.NotAfter,
+			},
+		}
+		enc := json.NewEncoder(w)
+		enc.SetIndent("", "    ")
+		if err := enc.Encode(rep); err != nil {
+			http.Error(w, "Unable to marshal response", http.StatusInternalServerError)
+			return
+		}
+	}
+}
diff --git a/go/pkg/cs/main.go b/go/pkg/cs/main.go
index 06722b02b..f2b6bc07f 100644
--- a/go/pkg/cs/main.go
+++ b/go/pkg/cs/main.go
@@ -18,7 +18,6 @@ package cs
 import (
 	"bytes"
 	"context"
-	"encoding/json"
 	"flag"
 	"fmt"
 	"hash"
@@ -409,27 +408,28 @@ func (app *App) Run() int {
 			log.Error("Loading client certificate chains failed", "err", err)
 			return 1
 		}
-		renewalHandler := trusthandler.ChainRenewalRequest{
-			Verifier: trusthandler.RenewalRequestVerifierFunc(
-				renewal.VerifyChainRenewalRequest),
-			ChainBuilder: cstrust.ChainBuilder{
-				PolicyGen: &cstrust.CachingPolicyGen{
-					PolicyGen: cstrust.LoadingPolicyGen{
-						Validity: 3 * 24 * time.Hour,
-						CertProvider: cstrust.CACertLoader{
-							IA:  topo.IA(),
-							DB:  trustDB,
-							Dir: filepath.Join(Cfg.General.ConfigDir, "crypto/ca"),
-						},
-						KeyRing: cstrust.LoadingRing{
-							Dir: filepath.Join(Cfg.General.ConfigDir, "crypto/ca"),
-						},
+		chainBuilder := cstrust.ChainBuilder{
+			PolicyGen: &cstrust.CachingPolicyGen{
+				PolicyGen: cstrust.LoadingPolicyGen{
+					Validity: Cfg.CA.MaxASValidity.Duration,
+					CertProvider: cstrust.CACertLoader{
+						IA:  topo.IA(),
+						DB:  trustDB,
+						Dir: filepath.Join(Cfg.General.ConfigDir, "crypto/ca"),
+					},
+					KeyRing: cstrust.LoadingRing{
+						Dir: filepath.Join(Cfg.General.ConfigDir, "crypto/ca"),
 					},
 				},
 			},
-			DB:     renewalDB,
-			IA:     topo.IA(),
-			Signer: signer,
+		}
+		renewalHandler := trusthandler.ChainRenewalRequest{
+			Verifier: trusthandler.RenewalRequestVerifierFunc(
+				renewal.VerifyChainRenewalRequest),
+			ChainBuilder: chainBuilder,
+			DB:           renewalDB,
+			IA:           topo.IA(),
+			Signer:       signer,
 		}
 
 		msgr.AddHandler(infra.ChainRenewalRequest,
@@ -437,6 +437,7 @@ func (app *App) Run() int {
 		tcpMsgr.AddHandler(infra.ChainRenewalRequest,
 			app.runHandlerWrapper(infra.ChainRenewalRequest, renewalHandler))
 
+		http.HandleFunc("/ca", caHandler(chainBuilder))
 	}
 
 	// Setup metrics and status pages
@@ -1017,55 +1018,3 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
 	toml.NewEncoder(&buf).Order(toml.OrderPreserve).Encode(Cfg)
 	fmt.Fprint(w, buf.String())
 }
-
-func signerHandler(signer cstrust.RenewingSigner) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Content-Type", "application/json")
-		s, err := signer.SignerGen.Generate(r.Context())
-		if err != nil {
-			http.Error(w, "Unable to get signer", http.StatusInternalServerError)
-			return
-		}
-
-		type Subject struct {
-			IA addr.IA `json:"isd_as"`
-		}
-		type TRCID struct {
-			ISD    addr.ISD        `json:"isd"`
-			Base   scrypto.Version `json:"base_number"`
-			Serial scrypto.Version `json:"serial_number"`
-		}
-		type Validity struct {
-			NotBefore time.Time `json:"not_before"`
-			NotAfter  time.Time `json:"not_after"`
-		}
-		rep := struct {
-			Subject       Subject   `json:"subject"`
-			SubjectKeyID  string    `json:"subject_key_id"`
-			Expiration    time.Time `json:"expiration"`
-			TRCID         TRCID     `json:"trc_id"`
-			ChainValidity Validity  `json:"chain_validity"`
-			InGrace       bool      `json:"in_grace_period"`
-		}{
-			Subject:      Subject{IA: s.IA},
-			SubjectKeyID: fmt.Sprintf("% X", s.SubjectKeyID),
-			Expiration:   s.Expiration,
-			TRCID: TRCID{
-				ISD:    s.TRCID.ISD,
-				Base:   s.TRCID.Base,
-				Serial: s.TRCID.Serial,
-			},
-			ChainValidity: Validity{
-				NotBefore: s.ChainValidity.NotBefore,
-				NotAfter:  s.ChainValidity.NotAfter,
-			},
-			InGrace: s.InGrace,
-		}
-		enc := json.NewEncoder(w)
-		enc.SetIndent("", "    ")
-		if err := enc.Encode(rep); err != nil {
-			http.Error(w, "Unable to marshal response", http.StatusInternalServerError)
-			return
-		}
-	}
-}
diff --git a/go/pkg/cs/trust/BUILD.bazel b/go/pkg/cs/trust/BUILD.bazel
index 057b4265e..02c1ab9a0 100644
--- a/go/pkg/cs/trust/BUILD.bazel
+++ b/go/pkg/cs/trust/BUILD.bazel
@@ -18,6 +18,7 @@ go_library(
         "//go/lib/scrypto:go_default_library",
         "//go/lib/scrypto/cppki:go_default_library",
         "//go/lib/serrors:go_default_library",
+        "//go/pkg/cs/trust/internal/metrics:go_default_library",
         "//go/pkg/trust:go_default_library",
         "//go/pkg/trust/renewal:go_default_library",
         "//go/proto:go_default_library",
diff --git a/go/pkg/cs/trust/ca_signer_gen.go b/go/pkg/cs/trust/ca_signer_gen.go
index 15763b9de..7b1cf8202 100644
--- a/go/pkg/cs/trust/ca_signer_gen.go
+++ b/go/pkg/cs/trust/ca_signer_gen.go
@@ -30,6 +30,7 @@ import (
 	"github.com/scionproto/scion/go/lib/scrypto"
 	"github.com/scionproto/scion/go/lib/scrypto/cppki"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/cs/trust/internal/metrics"
 	"github.com/scionproto/scion/go/pkg/trust"
 )
 
@@ -47,11 +48,19 @@ type ChainBuilder struct {
 func (c ChainBuilder) CreateChain(ctx context.Context,
 	csr *x509.CertificateRequest) ([]*x509.Certificate, error) {
 
+	l := metrics.SignerLabels{}
 	policy, err := c.PolicyGen.Generate(ctx)
 	if err != nil {
+		metrics.Signer.SignedChains(l.WithResult(metrics.ErrInactive)).Inc()
 		return nil, err
 	}
-	return policy.CreateChain(csr)
+	chain, err := policy.CreateChain(csr)
+	if err != nil {
+		metrics.Signer.SignedChains(l.WithResult(metrics.ErrInternal)).Inc()
+		return nil, err
+	}
+	metrics.Signer.SignedChains(l.WithResult(metrics.Success)).Inc()
+	return chain, nil
 }
 
 // CachingPolicyGen is a PolicyGen that can cache the previously generated
@@ -86,6 +95,7 @@ func (s *CachingPolicyGen) Generate(ctx context.Context) (cppki.CAPolicy, error)
 		s.ok = false
 		log.FromCtx(ctx).Info("Failed to generate a new CA policy, "+
 			"AS certificate signing not possible", "err", err)
+		metrics.Signer.ActiveCA().Set(0)
 		return cppki.CAPolicy{}, err
 	}
 	s.cached, s.ok = policy, true
@@ -93,6 +103,9 @@ func (s *CachingPolicyGen) Generate(ctx context.Context) (cppki.CAPolicy, error)
 		"subject_key_id", fmt.Sprintf("%x", policy.Certificate.SubjectKeyId),
 		"expiration", policy.Certificate.NotAfter,
 	)
+	metrics.Signer.ActiveCA().Set(1)
+	metrics.Signer.LastGeneratedCA().SetToCurrentTime()
+	metrics.Signer.ExpirationCA().Set(metrics.Timestamp(policy.Certificate.NotAfter))
 	return s.cached, nil
 }
 
@@ -116,19 +129,24 @@ type LoadingPolicyGen struct {
 // policy uses the private which is backed by the CA certificate with the
 // highest expiration time.
 func (g LoadingPolicyGen) Generate(ctx context.Context) (cppki.CAPolicy, error) {
+	l := metrics.SignerLabels{}
 	keys, err := g.KeyRing.PrivateKeys(ctx)
 	if err != nil {
+		metrics.Signer.GenerateCA(l.WithResult(metrics.ErrKey)).Inc()
 		return cppki.CAPolicy{}, err
 	}
 	if len(keys) == 0 {
+		metrics.Signer.GenerateCA(l.WithResult(metrics.ErrKey)).Inc()
 		return cppki.CAPolicy{}, serrors.New("no private key found")
 	}
 
 	certs, err := g.CertProvider.CACerts(ctx)
 	if err != nil {
+		metrics.Signer.GenerateCA(l.WithResult(metrics.ErrCerts)).Inc()
 		return cppki.CAPolicy{}, serrors.WrapStr("loading CA certificates", err)
 	}
 	if len(certs) == 0 {
+		metrics.Signer.GenerateCA(l.WithResult(metrics.ErrCerts)).Inc()
 		return cppki.CAPolicy{}, serrors.New("no CA certificate found")
 	}
 
@@ -150,9 +168,11 @@ func (g LoadingPolicyGen) Generate(ctx context.Context) (cppki.CAPolicy, error)
 		}
 	}
 	if bestCert == nil {
+		metrics.Signer.GenerateCA(l.WithResult(metrics.ErrNotFound)).Inc()
 		return cppki.CAPolicy{}, serrors.New("no CA certificate found",
 			"num_private_keys", len(keys))
 	}
+	metrics.Signer.GenerateCA(l.WithResult(metrics.Success)).Inc()
 	return cppki.CAPolicy{
 		Validity:    g.Validity,
 		Certificate: bestCert,
diff --git a/go/pkg/cs/trust/internal/metrics/BUILD.bazel b/go/pkg/cs/trust/internal/metrics/BUILD.bazel
index 6a7de323d..13daaaf9d 100644
--- a/go/pkg/cs/trust/internal/metrics/BUILD.bazel
+++ b/go/pkg/cs/trust/internal/metrics/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
     srcs = [
         "handler.go",
         "metrics.go",
+        "signer.go",
     ],
     importpath = "github.com/scionproto/scion/go/pkg/cs/trust/internal/metrics",
     visibility = ["//go/pkg/cs/trust:__subpackages__"],
diff --git a/go/pkg/cs/trust/internal/metrics/metrics.go b/go/pkg/cs/trust/internal/metrics/metrics.go
index 2537a09ba..e719d6581 100644
--- a/go/pkg/cs/trust/internal/metrics/metrics.go
+++ b/go/pkg/cs/trust/internal/metrics/metrics.go
@@ -59,6 +59,7 @@ const (
 	ErrInactive   = "err_inactive"
 	ErrInternal   = prom.ErrInternal
 	ErrKey        = "err_key"
+	ErrCerts      = "err_certs"
 	ErrNotAllowed = "err_not_allowed"
 	ErrNotFound   = "err_not_found"
 	ErrParse      = prom.ErrParse
@@ -70,6 +71,8 @@ const (
 var (
 	// Handler exposes the handler metrics.
 	Handler = newHandler()
+	// Signer exposes the signer metrics.
+	Signer = newSigner()
 )
 
 // PeerToLabel converts the peer address to a peer metric label.
diff --git a/go/pkg/cs/trust/internal/metrics/signer.go b/go/pkg/cs/trust/internal/metrics/signer.go
new file mode 100644
index 000000000..2421cc561
--- /dev/null
+++ b/go/pkg/cs/trust/internal/metrics/signer.go
@@ -0,0 +1,125 @@
+// Copyright 2020 Anapaya Systems
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package metrics
+
+import (
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+
+	"github.com/scionproto/scion/go/lib/prom"
+)
+
+// SignerLabels defines the trust material signer labels.
+type SignerLabels struct {
+	Result string
+}
+
+// Labels returns the list of labels.
+func (l SignerLabels) Labels() []string {
+	return []string{prom.LabelResult}
+}
+
+// Values returns the label values in the order defined by Labels.
+func (l SignerLabels) Values() []string {
+	return []string{l.Result}
+}
+
+// WithResult returns the lookup labels with the modified result.
+func (l SignerLabels) WithResult(result string) SignerLabels {
+	l.Result = result
+	return l
+}
+
+type signer struct {
+	caActive        prometheus.Gauge
+	caSigners       prometheus.CounterVec
+	signedChains    prometheus.CounterVec
+	lastGeneratedCA prometheus.Gauge
+	expirationCA    prometheus.Gauge
+	lastGeneratedAS prometheus.Gauge
+	expirationAS    prometheus.Gauge
+}
+
+func newSigner() signer {
+	return signer{
+		caActive: prom.NewGauge(Namespace, "",
+			"ca_signer_active_boolean",
+			"Whether the CA signer is active and can sign certificate chains",
+		),
+		caSigners: *prom.NewCounterVecWithLabels(Namespace, "",
+			"generated_ca_signers_total",
+			"Number of generated CA signers that sign certificate chains",
+			SignerLabels{},
+		),
+		signedChains: *prom.NewCounterVecWithLabels(Namespace, "",
+			"signed_certificate_chains_total",
+			"Number of certificate chains signed",
+			SignerLabels{},
+		),
+		lastGeneratedCA: prom.NewGauge(Namespace, "",
+			"last_ca_signer_generation_time_second",
+			"The last time a signer for creating AS certificates was successfully generated",
+		),
+		expirationCA: prom.NewGauge(Namespace, "",
+			"ca_signer_expiration_time_second",
+			"The expiration time of the current CA signer",
+		),
+		lastGeneratedAS: prom.NewGauge(Namespace, "",
+			"last_signer_generation_time_second",
+			"The last time a signer for control plane messages was successfully generated",
+		),
+		expirationAS: prom.NewGauge(Namespace, "",
+			"signer_expiration_time_second",
+			"The expiration time of the current signer",
+		),
+	}
+}
+
+func (s *signer) ActiveCA() prometheus.Gauge {
+	return s.caActive
+}
+
+func (s *signer) SignedChains(l SignerLabels) prometheus.Counter {
+	return s.signedChains.WithLabelValues(l.Values()...)
+}
+
+func (s *signer) GenerateCA(l SignerLabels) prometheus.Counter {
+	return s.caSigners.WithLabelValues(l.Values()...)
+}
+
+func (s *signer) LastGeneratedCA() prometheus.Gauge {
+	return s.lastGeneratedCA
+}
+
+func (s *signer) ExpirationCA() prometheus.Gauge {
+	return s.expirationCA
+}
+
+func (s *signer) LastGeneratedAS() prometheus.Gauge {
+	return s.lastGeneratedAS
+}
+
+func (s *signer) ExpirationAS() prometheus.Gauge {
+	return s.expirationAS
+}
+
+// Timestamp return the prometheus value for gauge.
+func Timestamp(ts time.Time) float64 {
+	if ts.IsZero() {
+		return 0
+	}
+	return float64(ts.UnixNano() / 1e9)
+}
diff --git a/go/pkg/cs/trust/signer_gen.go b/go/pkg/cs/trust/signer_gen.go
index f4b7a6c7e..f9667d057 100644
--- a/go/pkg/cs/trust/signer_gen.go
+++ b/go/pkg/cs/trust/signer_gen.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/scionproto/scion/go/lib/log"
 	"github.com/scionproto/scion/go/lib/serrors"
+	"github.com/scionproto/scion/go/pkg/cs/trust/internal/metrics"
 	"github.com/scionproto/scion/go/pkg/trust"
 )
 
@@ -65,5 +66,7 @@ func (s *CachingSignerGen) Generate(ctx context.Context) (trust.Signer, error) {
 		"subject_key_id", fmt.Sprintf("%x", signer.SubjectKeyID),
 		"expiration", signer.Expiration,
 	)
+	metrics.Signer.LastGeneratedAS().SetToCurrentTime()
+	metrics.Signer.ExpirationAS().Set(metrics.Timestamp(signer.Expiration))
 	return s.cached, nil
 }
diff --git a/go/pkg/trust/internal/metrics/signer.go b/go/pkg/trust/internal/metrics/signer.go
index 1fec9dbe4..f050d5b05 100644
--- a/go/pkg/trust/internal/metrics/signer.go
+++ b/go/pkg/trust/internal/metrics/signer.go
@@ -55,9 +55,6 @@ func newSigner() signer {
 			SignerLabels{}),
 		Signers: *prom.NewCounterVecWithLabels(Namespace, "", "generated_signers_total",
 			"Number of generated signers backed by the trust engine", SignerLabels{}),
-		ASNotAfter: prom.NewGauge(Namespace, "",
-			"latest_generated_signer_expiration_time_second",
-			"The notafter time of the AS certificate of the latest signer"),
 	}
 }
 
@@ -68,9 +65,6 @@ func (s *signer) Sign(l SignerLabels) prometheus.Counter {
 func (s *signer) Generate(l SignerLabels) prometheus.Counter {
 	return s.Signers.WithLabelValues(l.Values()...)
 }
-func (s *signer) NotAfter() prometheus.Gauge {
-	return s.ASNotAfter
-}
 
 // Timestamp return the prometheus value for gauge.
 func Timestamp(ts time.Time) float64 {
diff --git a/go/pkg/trust/signer_gen.go b/go/pkg/trust/signer_gen.go
index 485e31f24..2d1b9fd9f 100644
--- a/go/pkg/trust/signer_gen.go
+++ b/go/pkg/trust/signer_gen.go
@@ -46,6 +46,7 @@ func (s SignerGen) Generate(ctx context.Context) (Signer, error) {
 	l := metrics.SignerLabels{}
 	keys, err := s.KeyRing.PrivateKeys(ctx)
 	if err != nil {
+		metrics.Signer.Generate(l.WithResult(metrics.ErrKey)).Inc()
 		return Signer{}, err
 	}
 	if len(keys) == 0 {
@@ -79,8 +80,6 @@ func (s SignerGen) Generate(ctx context.Context) (Signer, error) {
 		metrics.Signer.Generate(l.WithResult(metrics.ErrNotFound)).Inc()
 		return Signer{}, serrors.New("no certificate found", "num_private_keys", len(keys))
 	}
-
-	metrics.Signer.NotAfter().Set(metrics.Timestamp(best.Expiration))
 	metrics.Signer.Generate(l.WithResult(metrics.Success)).Inc()
 	return *best, nil
 }


Test patch:
diff --git a/go/cs/config/config_test.go b/go/cs/config/config_test.go
index 988d1de68..d9abe2928 100644
--- a/go/cs/config/config_test.go
+++ b/go/cs/config/config_test.go
@@ -63,6 +63,7 @@ func InitTestConfig(cfg *Config) {
 	beaconstoragetest.InitTestBeaconDBConf(&cfg.BeaconDB)
 	pathstoragetest.InitTestPathDBConf(&cfg.PathDB)
 	InitTestBSConfig(&cfg.BS)
+	InitTestCA(&cfg.CA)
 }
 
 func InitTestBSConfig(cfg *BSConfig) {
@@ -84,6 +85,7 @@ func CheckTestConfig(t *testing.T, cfg *Config, id string) {
 	pathstoragetest.CheckTestPathDBConf(t, &cfg.PathDB, id)
 	CheckTestBSConfig(t, &cfg.BS)
 	CheckTestPSConfig(t, &cfg.PS, id)
+	CheckTestCA(t, &cfg.CA, id)
 }
 
 func CheckTestBSConfig(t *testing.T, cfg *BSConfig) {
@@ -105,9 +107,14 @@ func CheckTestPolicies(t *testing.T, cfg *Policies) {
 	assert.Empty(t, cfg.DownRegistration)
 }
 
-func InitTestPSConfig(cfg *PSConfig) {
-}
+func InitTestPSConfig(cfg *PSConfig) {}
 
 func CheckTestPSConfig(t *testing.T, cfg *PSConfig, id string) {
 	assert.Equal(t, DefaultQueryInterval, cfg.QueryInterval.Duration)
 }
+
+func InitTestCA(cfg *CA) {}
+
+func CheckTestCA(t *testing.T, cfg *CA, id string) {
+	assert.Equal(t, DefaultMaxASValidity, cfg.MaxASValidity.Duration)
+}
diff --git a/go/pkg/trust/signer_gen_test.go b/go/pkg/trust/signer_gen_test.go
index ccde21340..4b3dc24dd 100644
--- a/go/pkg/trust/signer_gen_test.go
+++ b/go/pkg/trust/signer_gen_test.go
@@ -383,14 +383,5 @@ trustengine_generated_signers_total{result="ok_success"} 4
 `, s, s)
 		err = testutil.CollectAndCompare(metrics.Signer.Signers, strings.NewReader(want))
 		require.NoError(t, err)
-
-		s1 := "trustengine_latest_generated_signer_expiration_time_second"
-		want1 := fmt.Sprintf(`
-# HELP %s The notafter time of the AS certificate of the latest signer
-# TYPE %s gauge
-%s %g
-`, s1, s1, s1, float64(chain[0].NotAfter.UnixNano())/1e9)
-		err = testutil.CollectAndCompare(metrics.Signer.ASNotAfter, strings.NewReader(want1))
-		require.NoError(t, err)
 	})
 }