Skip to content

Commit

Permalink
Merge pull request #3062 from corhere/renew-certs-tool
Browse files Browse the repository at this point in the history
rafttool: add tool to renew cert on a manager node
  • Loading branch information
dperny committed Nov 2, 2022
2 parents ef3b414 + c9be182 commit 6341884
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 3 deletions.
4 changes: 2 additions & 2 deletions ca/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ func (s *Server) UpdateRootCA(ctx context.Context, cluster *api.Cluster, reconci
log.G(ctx).Warn("no certificate expiration specified, using default")
}
// Attempt to update our local RootCA with the new parameters
updatedRootCA, err := RootCAFromAPI(ctx, rCA, expiry)
updatedRootCA, err := RootCAFromAPI(rCA, expiry)
if err != nil {
return errors.Wrap(err, "invalid Root CA object in cluster")
}
Expand Down Expand Up @@ -901,7 +901,7 @@ func isFinalState(status api.IssuanceStatus) bool {
}

// RootCAFromAPI creates a RootCA object from an api.RootCA object
func RootCAFromAPI(ctx context.Context, apiRootCA *api.RootCA, expiry time.Duration) (RootCA, error) {
func RootCAFromAPI(apiRootCA *api.RootCA, expiry time.Duration) (RootCA, error) {
var intermediates []byte
signingCert := apiRootCA.CACert
signingKey := apiRootCA.CAKey
Expand Down
19 changes: 19 additions & 0 deletions cmd/swarm-rafttool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@ var (
return downgradeKey(stateDir, unlockKey)
},
}

renewCertsCmd = &cobra.Command{
Use: "renew-certs",
Short: "Renew expired manager cert",
RunE: func(cmd *cobra.Command, args []string) error {
stateDir, err := cmd.Flags().GetString("state-dir")
if err != nil {
return err
}

unlockKey, err := cmd.Flags().GetString("unlock-key")
if err != nil {
return err
}

return renewCerts(stateDir, unlockKey)
},
}
)

func init() {
Expand All @@ -169,6 +187,7 @@ func init() {
dumpSnapshotCmd,
dumpObjectCmd,
downgradeKeyCmd,
renewCertsCmd,
)

dumpSnapshotCmd.Flags().Bool("redact", false, "Redact the values of secrets, configs, and environment variables")
Expand Down
101 changes: 101 additions & 0 deletions cmd/swarm-rafttool/renewcert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"fmt"

"github.com/cloudflare/cfssl/helpers"
"github.com/gogo/protobuf/proto"
"github.com/gogo/protobuf/types"
"github.com/pkg/errors"
"go.etcd.io/etcd/raft/v3/raftpb"

"github.com/moby/swarmkit/v2/api"
"github.com/moby/swarmkit/v2/ca"
)

func renewCerts(swarmdir, unlockKey string) error {
// First, load the existing cert. We don't actually bother to check if
// it's expired - this will just obtain a new cert anyway.
krw, err := getKRW(swarmdir, unlockKey)
if err != nil {
return errors.Wrap(err, "could not load swarm certificate")
}
cert, _, err := krw.Read()
if err != nil {
return errors.Wrap(err, "could not read swarm certificate")
}
certificates, err := helpers.ParseCertificatesPEM(cert)
if err != nil {
return errors.Wrap(err, "could not parse node certificate")
}
// We need to make sure when renewing that we provide the same CN (node ID),
// OU (role), and org (swarm cluster ID) when getting a new certificate
var (
cn string = certificates[0].Subject.CommonName
ou string = certificates[0].Subject.OrganizationalUnit[0]
org string = certificates[0].Subject.Organization[0]
)

// Load up the raft data on disk
walData, snapshot, err := loadData(swarmdir, unlockKey)
if err != nil {
return errors.Wrap(err, "could not load swarm data")
}
var cluster *api.Cluster

// If there's a snapshot, get the cluster from it
if snapshot != nil {
s := &api.Snapshot{}
if err := proto.Unmarshal(snapshot.Data, s); err != nil {
return err
}
if s.Version != api.Snapshot_V0 {
return fmt.Errorf("unrecognized snapshot version %d", s.Version)
}
cluster = s.Store.Clusters[0]
}

// It's possible there's no snapshot yet, or the cluster has been updated
// since the last snapshot, so also read from the WALs
for _, ent := range walData.Entries {
if ent.Type != raftpb.EntryNormal {
continue
}

r := &api.InternalRaftRequest{}
err := proto.Unmarshal(ent.Data, r)
if err != nil {
return errors.Wrap(err, "could not read WAL")
}

for _, act := range r.Action {
target := act.GetTarget()
if actype, ok := target.(*api.StoreAction_Cluster); ok {
cluster = actype.Cluster
}
}
}

// There should always be a cluster and CA cert, unless the raft store has been
// catastrophcially corrupted, but it's possible that there is no CA key because
// the cluster used an external CA.
if cluster == nil || cluster.RootCA.CACert == nil || cluster.RootCA.CAKey == nil {
return errors.New("could not find CA key data in raft logs; cannot renew certs")
}

// Issue a new certificate that expires at the configured expiry time.
expiry := ca.DefaultNodeCertExpiration
if cluster.Spec.CAConfig.NodeCertExpiry != nil {
clusterExpiry, err := types.DurationFromProto(cluster.Spec.CAConfig.NodeCertExpiry)
if err == nil {
expiry = clusterExpiry
}
}
rootCA, err := ca.RootCAFromAPI(&cluster.RootCA, expiry)
if err != nil {
return errors.Wrap(err, "invalid CA info in raft logs; cannot renew certs")
}

_, _, err = rootCA.IssueAndSaveNewCertificates(krw, cn, ou, org)
return err
}
2 changes: 1 addition & 1 deletion manager/controlapi/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (s *Server) UpdateCluster(ctx context.Context, request *api.UpdateClusterRe
}
// This ensures that we have the current rootCA with which to generate tokens (expiration doesn't matter
// for generating the tokens)
rootCA, err := ca.RootCAFromAPI(ctx, &cluster.RootCA, ca.DefaultNodeCertExpiration)
rootCA, err := ca.RootCAFromAPI(&cluster.RootCA, ca.DefaultNodeCertExpiration)
if err != nil {
log.G(ctx).WithField(
"method", "(*controlapi.Server).UpdateCluster").WithError(err).Error("invalid cluster root CA")
Expand Down

0 comments on commit 6341884

Please sign in to comment.