diff --git a/cmd/osctl/cmd/gen.go b/cmd/osctl/cmd/gen.go index ef80d3b2b..ce04b5715 100644 --- a/cmd/osctl/cmd/gen.go +++ b/cmd/osctl/cmd/gen.go @@ -191,7 +191,7 @@ func init() { // Certificate Authorities caCmd.Flags().StringVar(&organization, "organization", "", "X.509 distinguished name for the Organization") helpers.Should(cobra.MarkFlagRequired(caCmd.Flags(), "organization")) - caCmd.Flags().IntVar(&hours, "hours", 24, "the hours from now on which the certificate validity period ends") + caCmd.Flags().IntVar(&hours, "hours", 87600, "the hours from now on which the certificate validity period ends") caCmd.Flags().BoolVar(&rsa, "rsa", false, "generate in RSA format") // Keys keyCmd.Flags().StringVar(&name, "name", "", "the basename of the generated file") diff --git a/cmd/osctl/cmd/inject.go b/cmd/osctl/cmd/inject.go index f96bb2472..28044a036 100644 --- a/cmd/osctl/cmd/inject.go +++ b/cmd/osctl/cmd/inject.go @@ -36,19 +36,6 @@ var injectOSCmd = &cobra.Command{ }, } -// injectIdentityCmd represents the inject command -// nolint: dupl -var injectIdentityCmd = &cobra.Command{ - Use: "identity", - Short: "inject identity data.", - Long: ``, - Run: func(cmd *cobra.Command, args []string) { - if err := inject(args, crt, key, injectIdentityData); err != nil { - helpers.Fatalf("%s", err) - } - }, -} - // injectKubernetesCmd represents the inject command // nolint: dupl var injectKubernetesCmd = &cobra.Command{ @@ -76,20 +63,6 @@ func injectOSData(u *userdata.UserData, crt, key string) (err error) { return nil } -// nolint: dupl -func injectIdentityData(u *userdata.UserData, crt, key string) (err error) { - if u.Security == nil { - u.Security = newSecurity() - } - crtAndKey, err := x509.NewCertificateAndKeyFromFiles(crt, key) - if err != nil { - return - } - u.Security.OS.Identity = crtAndKey - - return nil -} - // nolint: dupl func injectKubernetesData(u *userdata.UserData, crt, key string) (err error) { if u.Security == nil { @@ -144,19 +117,14 @@ func newSecurity() *userdata.Security { func init() { injectOSCmd.Flags().StringVar(&crt, "crt", "", "the path to the PKI certificate") - injectIdentityCmd.Flags().StringVar(&crt, "crt", "", "the path to the PKI certificate") injectKubernetesCmd.Flags().StringVar(&crt, "crt", "", "the path to the PKI certificate") helpers.Should(injectOSCmd.MarkFlagRequired("crt")) - helpers.Should(injectIdentityCmd.MarkFlagRequired("crt")) helpers.Should(injectKubernetesCmd.MarkFlagRequired("crt")) injectOSCmd.Flags().StringVar(&key, "key", "", "the path to the PKI key") - injectIdentityCmd.Flags().StringVar(&key, "key", "", "the path to the PKI key") injectKubernetesCmd.Flags().StringVar(&key, "key", "", "the path to the PKI key") helpers.Should(injectOSCmd.MarkFlagRequired("key")) - helpers.Should(injectIdentityCmd.MarkFlagRequired("key")) helpers.Should(injectKubernetesCmd.MarkFlagRequired("key")) - injectCmd.AddCommand(injectOSCmd, injectIdentityCmd, injectKubernetesCmd) rootCmd.AddCommand(injectCmd) } diff --git a/cmd/osctl/cmd/root.go b/cmd/osctl/cmd/root.go index 84ac8e345..30964b5bb 100644 --- a/cmd/osctl/cmd/root.go +++ b/cmd/osctl/cmd/root.go @@ -101,14 +101,11 @@ func Execute() { // setupClient wraps common code to initialize osd client func setupClient(action func(*client.Client)) { - creds, err := client.NewDefaultClientCredentials(talosconfig) + t, creds, err := client.NewClientTargetAndCredentialsFromConfig(talosconfig) if err != nil { helpers.Fatalf("error getting client credentials: %s", err) } - if target != "" { - creds.Target = target - } - c, err := client.NewClient(constants.OsdPort, creds) + c, err := client.NewClient(creds, t, constants.OsdPort) if err != nil { helpers.Fatalf("error constructing client: %s", err) } diff --git a/cmd/osctl/pkg/client/client.go b/cmd/osctl/pkg/client/client.go index 33bcc85b6..67a5a3ae2 100644 --- a/cmd/osctl/pkg/client/client.go +++ b/cmd/osctl/pkg/client/client.go @@ -30,10 +30,9 @@ import ( // Credentials represents the set of values required to initialize a vaild // Client. type Credentials struct { - Target string - ca []byte - crt []byte - key []byte + ca []byte + crt []byte + key []byte } // Client implements the proto.OSClient interface. It serves as the @@ -46,70 +45,84 @@ type Client struct { NetworkClient networkapi.NetworkClient } -// NewDefaultClientCredentials initializes ClientCredentials using default paths +// NewClientTargetAndCredentialsFromConfig initializes ClientCredentials using default paths // to the required CA, certificate, and key. -func NewDefaultClientCredentials(p string) (creds *Credentials, err error) { +func NewClientTargetAndCredentialsFromConfig(p string) (target string, creds *Credentials, err error) { c, err := config.Open(p) if err != nil { return } if c.Context == "" { - return nil, fmt.Errorf("'context' key is not set in the config") + return "", nil, fmt.Errorf("'context' key is not set in the config") } context := c.Contexts[c.Context] if context == nil { - return nil, fmt.Errorf("context %q is not defined in 'contexts' key in config", c.Context) + return "", nil, fmt.Errorf("context %q is not defined in 'contexts' key in config", c.Context) } caBytes, err := base64.StdEncoding.DecodeString(context.CA) if err != nil { - return - } - crtBytes, err := base64.StdEncoding.DecodeString(context.Crt) - if err != nil { - return - } - keyBytes, err := base64.StdEncoding.DecodeString(context.Key) - if err != nil { - return - } - creds = &Credentials{ - Target: context.Target, - ca: caBytes, - crt: crtBytes, - key: keyBytes, + return "", nil, fmt.Errorf("error decoding CA: %v", err) } - return creds, nil + crtBytes, err := base64.StdEncoding.DecodeString(context.Crt) + if err != nil { + return "", nil, fmt.Errorf("error decoding certificate: %v", err) + } + + keyBytes, err := base64.StdEncoding.DecodeString(context.Key) + if err != nil { + return "", nil, fmt.Errorf("error decoding key: %v", err) + } + + creds = &Credentials{ + ca: caBytes, + crt: crtBytes, + key: keyBytes, + } + + return context.Target, creds, nil +} + +// NewClientCredentials initializes ClientCredentials using default paths +// to the required CA, certificate, and key. +func NewClientCredentials(ca, crt, key []byte) (creds *Credentials) { + creds = &Credentials{ + ca: ca, + crt: crt, + key: key, + } + + return creds } // NewClient initializes a Client. -func NewClient(port int, clientcreds *Credentials) (c *Client, err error) { +func NewClient(creds *Credentials, target string, port int) (c *Client, err error) { grpcOpts := []grpc.DialOption{} c = &Client{} - crt, err := tls.X509KeyPair(clientcreds.crt, clientcreds.key) + crt, err := tls.X509KeyPair(creds.crt, creds.key) if err != nil { return nil, fmt.Errorf("could not load client key pair: %s", err) } certPool := x509.NewCertPool() - if ok := certPool.AppendCertsFromPEM(clientcreds.ca); !ok { + if ok := certPool.AppendCertsFromPEM(creds.ca); !ok { return nil, fmt.Errorf("failed to append client certs") } // TODO(andrewrynhard): Do not parse the address. Pass the IP and port in as separate // parameters. - creds := credentials.NewTLS(&tls.Config{ - ServerName: clientcreds.Target, + transportCreds := credentials.NewTLS(&tls.Config{ + ServerName: target, Certificates: []tls.Certificate{crt}, // Set the root certificate authorities to use the self-signed // certificate. RootCAs: certPool, }) - grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(creds)) - c.conn, err = grpc.Dial(fmt.Sprintf("%s:%d", net.FormatAddress(clientcreds.Target), port), grpcOpts...) + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(transportCreds)) + c.conn, err = grpc.Dial(fmt.Sprintf("%s:%d", net.FormatAddress(target), port), grpcOpts...) if err != nil { return } diff --git a/internal/app/machined/internal/phase/userdata/pki.go b/internal/app/machined/internal/phase/userdata/pki.go deleted file mode 100644 index 8ceca1968..000000000 --- a/internal/app/machined/internal/phase/userdata/pki.go +++ /dev/null @@ -1,61 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package userdata - -import ( - "log" - - "github.com/pkg/errors" - - "github.com/talos-systems/talos/internal/app/machined/internal/phase" - "github.com/talos-systems/talos/internal/pkg/platform" - "github.com/talos-systems/talos/internal/pkg/runtime" - "github.com/talos-systems/talos/pkg/constants" - "github.com/talos-systems/talos/pkg/crypto/x509" - "github.com/talos-systems/talos/pkg/grpc/gen" - "github.com/talos-systems/talos/pkg/userdata" -) - -// PKI represents the PKI task. -type PKI struct{} - -// NewPKITask initializes and returns an UserData task. -func NewPKITask() phase.Task { - return &PKI{} -} - -// RuntimeFunc returns the runtime function. -func (task *PKI) RuntimeFunc(mode runtime.Mode) phase.RuntimeFunc { - return task.runtime -} - -func (task *PKI) runtime(platform platform.Platform, data *userdata.UserData) (err error) { - if data.Services.Kubeadm.IsControlPlane() { - log.Println("generating PKI locally") - var csr *x509.CertificateSigningRequest - if csr, err = data.NewIdentityCSR(); err != nil { - return err - } - var crt *x509.Certificate - crt, err = x509.NewCertificateFromCSRBytes(data.Security.OS.CA.Crt, data.Security.OS.CA.Key, csr.X509CertificateRequestPEM) - if err != nil { - return err - } - data.Security.OS.Identity.Crt = crt.X509CertificatePEM - return nil - } - - log.Println("generating PKI from trustd") - var generator *gen.Generator - generator, err = gen.NewGenerator(data, constants.TrustdPort) - if err != nil { - return errors.Wrap(err, "failed to create trustd client") - } - if err = generator.Identity(data); err != nil { - return errors.Wrap(err, "failed to generate identity") - } - - return nil -} diff --git a/internal/app/machined/internal/sequencer/v1alpha1/types.go b/internal/app/machined/internal/sequencer/v1alpha1/types.go index 08d978ede..abda061ac 100644 --- a/internal/app/machined/internal/sequencer/v1alpha1/types.go +++ b/internal/app/machined/internal/sequencer/v1alpha1/types.go @@ -64,7 +64,6 @@ func (d *Sequencer) Boot() error { ), phase.NewPhase( "user requests", - userdatatask.NewPKITask(), userdatatask.NewExtraEnvVarsTask(), userdatatask.NewExtraFilesTask(), ), diff --git a/internal/app/machined/pkg/system/services/kubeadm/kubeadm.go b/internal/app/machined/pkg/system/services/kubeadm/kubeadm.go index 1e4389654..b47f5f197 100644 --- a/internal/app/machined/pkg/system/services/kubeadm/kubeadm.go +++ b/internal/app/machined/pkg/system/services/kubeadm/kubeadm.go @@ -221,18 +221,12 @@ func FileSet(files []string) []*securityapi.ReadFileRequest { return fileRequests } -// CreateSecurityClients handles instantiating a trustd client connection +// CreateSecurityClients handles instantiating a security API client connection // to each trustd endpoint defined in userdata -func CreateSecurityClients(data *userdata.UserData) ([]securityapi.SecurityClient, error) { - var creds basic.Credentials - var err error +func CreateSecurityClients(data *userdata.UserData) (clients []securityapi.SecurityClient, err error) { + clients = []securityapi.SecurityClient{} - trustds := []securityapi.SecurityClient{} - - creds, err = basic.NewCredentials(data.Services.Trustd) - if err != nil { - return trustds, err - } + creds := basic.NewTokenCredentials(data.Services.Trustd.Token) // Create a trustd client for each endpoint to set up // a fan out approach to gathering the files @@ -240,14 +234,14 @@ func CreateSecurityClients(data *userdata.UserData) ([]securityapi.SecurityClien for _, endpoint := range data.Services.Trustd.Endpoints { conn, err = basic.NewConnection(endpoint, constants.TrustdPort, creds) if err != nil { - return trustds, err + return clients, err } - trustds = append(trustds, securityapi.NewSecurityClient(conn)) + clients = append(clients, securityapi.NewSecurityClient(conn)) } - return trustds, nil + return clients, nil } -// Download handles the retrieval of files from a trustd endpoint +// Download handles the retrieval of files from a security API endpoint. func Download(ctx context.Context, client securityapi.SecurityClient, file *securityapi.ReadFileRequest, content chan<- []byte) { select { case <-ctx.Done(): diff --git a/internal/app/osd/main.go b/internal/app/osd/main.go index acae59581..75dfd5192 100644 --- a/internal/app/osd/main.go +++ b/internal/app/osd/main.go @@ -5,9 +5,10 @@ package main import ( - "context" "flag" "log" + stdlibnet "net" + "os" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -16,6 +17,7 @@ import ( "github.com/talos-systems/talos/pkg/constants" "github.com/talos-systems/talos/pkg/grpc/factory" "github.com/talos-systems/talos/pkg/grpc/tls" + "github.com/talos-systems/talos/pkg/net" "github.com/talos-systems/talos/pkg/startup" "github.com/talos-systems/talos/pkg/userdata" ) @@ -28,9 +30,10 @@ func init() { flag.Parse() } +// nolint: gocyclo func main() { if err := startup.RandSeed(); err != nil { - log.Fatalf("startup: %s", err) + log.Fatalf("failed to seed RNG: %s", err) } data, err := userdata.Open(*dataPath) @@ -38,40 +41,63 @@ func main() { log.Fatalf("open user data: %v", err) } - tlsCertProvider, err := tls.NewRenewingFileCertificateProvider(context.TODO(), data) + ips, err := net.IPAddrs() if err != nil { - log.Fatalln("failed to create new dynamic certificate provider:", err) + log.Fatalf("failed to discover IP addresses: %+v", err) } - config, err := tls.NewConfigWithOpts( + // TODO(andrewrynhard): Allow for DNS names. + for _, san := range data.Services.Trustd.CertSANs { + if ip := stdlibnet.ParseIP(san); ip != nil { + ips = append(ips, ip) + } + } + + hostname, err := os.Hostname() + if err != nil { + log.Fatalf("failed to discover hostname: %+v", err) + } + + var provider tls.CertificateProvider + provider, err = tls.NewRemoteRenewingFileCertificateProvider(data.Services.Trustd.Token, data.Services.Trustd.Endpoints, constants.TrustdPort, hostname, ips) + if err != nil { + log.Fatalf("failed to create remote certificate provider: %+v", err) + } + + ca, err := provider.GetCA() + if err != nil { + log.Fatalf("failed to get root CA: %+v", err) + } + + config, err := tls.New( tls.WithClientAuthType(tls.Mutual), - tls.WithCACertPEM(data.Security.OS.CA.Crt), - tls.WithCertificateProvider(tlsCertProvider)) + tls.WithCACertPEM(ca), + tls.WithCertificateProvider(provider), + ) if err != nil { log.Fatalf("failed to create OS-level TLS configuration: %v", err) } - MachineClient, err := reg.NewMachineClient() + machineClient, err := reg.NewMachineClient() if err != nil { log.Fatalf("init client: %v", err) } - TimeClient, err := reg.NewTimeClient() + timeClient, err := reg.NewTimeClient() if err != nil { log.Fatalf("ntp client: %v", err) } - NetworkClient, err := reg.NewNetworkClient() + networkClient, err := reg.NewNetworkClient() if err != nil { log.Fatalf("networkd client: %v", err) } - log.Println("Starting osd") err = factory.ListenAndServe( ®.Registrator{ Data: data, - MachineClient: MachineClient, - TimeClient: TimeClient, - NetworkClient: NetworkClient, + MachineClient: machineClient, + TimeClient: timeClient, + NetworkClient: networkClient, }, factory.Port(constants.OsdPort), factory.ServerOptions( diff --git a/internal/app/trustd/main.go b/internal/app/trustd/main.go index 6ea521c74..1912e2cbf 100644 --- a/internal/app/trustd/main.go +++ b/internal/app/trustd/main.go @@ -7,6 +7,8 @@ package main import ( "flag" "log" + stdlibnet "net" + "os" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -16,6 +18,7 @@ import ( "github.com/talos-systems/talos/pkg/grpc/factory" "github.com/talos-systems/talos/pkg/grpc/middleware/auth/basic" "github.com/talos-systems/talos/pkg/grpc/tls" + "github.com/talos-systems/talos/pkg/net" "github.com/talos-systems/talos/pkg/startup" "github.com/talos-systems/talos/pkg/userdata" ) @@ -28,6 +31,7 @@ func init() { flag.Parse() } +// nolint: gocyclo func main() { var err error @@ -37,18 +41,45 @@ func main() { data, err := userdata.Open(*dataPath) if err != nil { - log.Fatalf("credentials: %v", err) + log.Fatalf("failed to open machine config: %v", err) } - config, err := tls.NewConfig(tls.ServerOnly, data.Security.OS) - if err != nil { - log.Fatalf("credentials: %v", err) - } - - creds, err := basic.NewCredentials(data.Services.Trustd) + ips, err := net.IPAddrs() if err != nil { log.Fatal(err) } + for _, san := range data.Services.Trustd.CertSANs { + if ip := stdlibnet.ParseIP(san); ip != nil { + ips = append(ips, ip) + } + } + + hostname, err := os.Hostname() + if err != nil { + log.Fatal(err) + } + + var provider tls.CertificateProvider + provider, err = tls.NewLocalRenewingFileCertificateProvider(data.Security.OS.CA.Key, data.Security.OS.CA.Crt, hostname, ips) + if err != nil { + log.Fatalln("failed to create local certificate provider:", err) + } + + ca, err := provider.GetCA() + if err != nil { + log.Fatal(err) + } + + config, err := tls.New( + tls.WithClientAuthType(tls.ServerOnly), + tls.WithCACertPEM(ca), + tls.WithCertificateProvider(provider), + ) + if err != nil { + log.Fatalf("failed to create TLS config: %v", err) + } + + creds := basic.NewTokenCredentials(data.Services.Trustd.Token) err = factory.ListenAndServe( ®.Registrator{Data: data.Security.OS}, diff --git a/internal/pkg/platform/iso/iso.go b/internal/pkg/platform/iso/iso.go index c75427c50..323207d27 100644 --- a/internal/pkg/platform/iso/iso.go +++ b/internal/pkg/platform/iso/iso.go @@ -23,8 +23,7 @@ func (i *ISO) UserData() (data *userdata.UserData, err error) { data = &userdata.UserData{ Security: &userdata.Security{ OS: &userdata.OSSecurity{ - CA: &x509.PEMEncodedCertificateAndKey{}, - Identity: &x509.PEMEncodedCertificateAndKey{}, + CA: &x509.PEMEncodedCertificateAndKey{}, }, Kubernetes: &userdata.KubernetesSecurity{ CA: &x509.PEMEncodedCertificateAndKey{}, diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 15d10cfe1..fc715dd10 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -175,11 +175,8 @@ const ( // RootfsAsset defines a well known name for our rootfs filename RootfsAsset = "rootfs.sqsh" - // NodeCertFile is the filename where the current Talos Node Certificate may be found - NodeCertFile = SystemRunPath + "/talos-node.crt" - - // NodeCertRenewalInterval is the default interval at which Talos Node Certifications should be renewed - NodeCertRenewalInterval = 24 * time.Hour + // DefaultCertificateValidityDuration is the default duration for a certificate. + DefaultCertificateValidityDuration = 24 * time.Hour // SystemVarPath is the path to write runtime system related files and // directories. diff --git a/pkg/crypto/x509/x509.go b/pkg/crypto/x509/x509.go index 39f6b263b..1195bb3f6 100644 --- a/pkg/crypto/x509/x509.go +++ b/pkg/crypto/x509/x509.go @@ -22,6 +22,8 @@ import ( "net" "strings" "time" + + "github.com/talos-systems/talos/pkg/constants" ) // CertificateAuthority represents a CA. @@ -136,7 +138,7 @@ func NewDefaultOptions(setters ...Option) *Options { DNSNames: []string{}, Bits: 4096, RSA: false, - NotAfter: time.Now().Add(24 * time.Hour), + NotAfter: time.Now().Add(constants.DefaultCertificateValidityDuration), } for _, setter := range setters { @@ -385,6 +387,41 @@ func NewCertificateAndKeyFromFiles(crt, key string) (p *PEMEncodedCertificateAnd return p, nil } +// NewCSRAndIdentity generates and PEM encoded certificate and key, along with a +// CSR for the generated key. +func NewCSRAndIdentity(hostname string, ips []net.IP) (csr *CertificateSigningRequest, identity *PEMEncodedCertificateAndKey, err error) { + var key *Key + key, err = NewKey() + if err != nil { + return nil, nil, err + } + + identity = &PEMEncodedCertificateAndKey{ + Key: key.KeyPEM, + } + + pemBlock, _ := pem.Decode(key.KeyPEM) + if pemBlock == nil { + return nil, nil, fmt.Errorf("failed to decode key") + } + + keyEC, err := x509.ParseECPrivateKey(pemBlock.Bytes) + if err != nil { + return nil, nil, err + } + + opts := []Option{} + opts = append(opts, DNSNames([]string{hostname})) + opts = append(opts, IPAddresses(ips)) + + csr, err = NewCertificateSigningRequest(keyEC, opts...) + if err != nil { + return nil, nil, err + } + + return csr, identity, nil +} + // UnmarshalYAML implements the yaml.Unmarshaler interface for // PEMEncodedCertificateAndKey. It is expected that the Crt and Key are a base64 // encoded string in the YAML file. This function decodes the strings into byte diff --git a/pkg/grpc/gen/local.go b/pkg/grpc/gen/local.go new file mode 100644 index 000000000..cf4c35428 --- /dev/null +++ b/pkg/grpc/gen/local.go @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package gen + +import ( + "github.com/talos-systems/talos/pkg/crypto/x509" +) + +// LocalGenerator represents the OS identity generator. +type LocalGenerator struct { + caKey []byte + caCrt []byte +} + +// NewLocalGenerator initializes a LocalGenerator. +func NewLocalGenerator(caKey, caCrt []byte) (g *LocalGenerator, err error) { + g = &LocalGenerator{caKey, caCrt} + + return g, nil +} + +// Identity creates an identity certificate using a local root CA. +func (g *LocalGenerator) Identity(csr *x509.CertificateSigningRequest) (ca, crt []byte, err error) { + var c *x509.Certificate + c, err = x509.NewCertificateFromCSRBytes(g.caCrt, g.caKey, csr.X509CertificateRequestPEM) + if err != nil { + return ca, crt, err + } + + crt = c.X509CertificatePEM + + return g.caCrt, crt, nil +} diff --git a/pkg/grpc/gen/gen.go b/pkg/grpc/gen/remote.go similarity index 51% rename from pkg/grpc/gen/gen.go rename to pkg/grpc/gen/remote.go index beb9887fe..8b0f843f9 100644 --- a/pkg/grpc/gen/gen.go +++ b/pkg/grpc/gen/remote.go @@ -16,37 +16,33 @@ import ( securityapi "github.com/talos-systems/talos/api/security" "github.com/talos-systems/talos/pkg/crypto/x509" "github.com/talos-systems/talos/pkg/grpc/middleware/auth/basic" - "github.com/talos-systems/talos/pkg/userdata" ) -// Generator represents the OS identity generator. -type Generator struct { +// RemoteGenerator represents the OS identity generator. +type RemoteGenerator struct { client securityapi.SecurityClient } -// NewGenerator initializes a Generator with a preconfigured grpc.ClientConn. -func NewGenerator(data *userdata.UserData, port int) (g *Generator, err error) { - if len(data.Services.Trustd.Endpoints) == 0 { +// NewRemoteGenerator initializes a RemoteGenerator with a preconfigured grpc.ClientConn. +func NewRemoteGenerator(token string, endpoints []string, port int) (g *RemoteGenerator, err error) { + if len(endpoints) == 0 { return nil, fmt.Errorf("at least one root of trust endpoint is required") } - creds, err := basic.NewCredentials(data.Services.Trustd) - if err != nil { - return nil, err - } + creds := basic.NewTokenCredentials(token) // Loop through trustd endpoints and attempt to download PKI var conn *grpc.ClientConn var multiError *multierror.Error - for i := 0; i < len(data.Services.Trustd.Endpoints); i++ { - conn, err = basic.NewConnection(data.Services.Trustd.Endpoints[i], port, creds) + for i := 0; i < len(endpoints); i++ { + conn, err = basic.NewConnection(endpoints[i], port, creds) if err != nil { multiError = multierror.Append(multiError, err) // Unable to connect, bail and attempt to contact next endpoint continue } client := securityapi.NewSecurityClient(conn) - return &Generator{client: client}, nil + return &RemoteGenerator{client: client}, nil } // We were unable to connect to any trustd endpoint @@ -55,35 +51,31 @@ func NewGenerator(data *userdata.UserData, port int) (g *Generator, err error) { } // Certificate implements the securityapi.SecurityClient interface. -func (g *Generator) Certificate(in *securityapi.CertificateRequest) (resp *securityapi.CertificateResponse, err error) { +func (g *RemoteGenerator) Certificate(in *securityapi.CertificateRequest) (resp *securityapi.CertificateResponse, err error) { ctx := context.Background() resp, err = g.client.Certificate(ctx, in) if err != nil { - return + return nil, err } return resp, err } -// Identity creates a CSR and sends it to trustd for signing. -// A signed certificate is returned. -func (g *Generator) Identity(data *userdata.UserData) (err error) { - if data.Security == nil { - data.Security = &userdata.Security{} - } - data.Security.OS = &userdata.OSSecurity{CA: &x509.PEMEncodedCertificateAndKey{}} - var csr *x509.CertificateSigningRequest - if csr, err = data.NewIdentityCSR(); err != nil { - return err - } +// Identity creates an identity certificate via the security API. +func (g *RemoteGenerator) Identity(csr *x509.CertificateSigningRequest) (ca, crt []byte, err error) { req := &securityapi.CertificateRequest{ Csr: csr.X509CertificateRequestPEM, } - return poll(g, req, data.Security.OS) + ca, crt, err = g.poll(req) + if err != nil { + return nil, nil, err + } + + return ca, crt, nil } -func poll(g *Generator, in *securityapi.CertificateRequest, data *userdata.OSSecurity) (err error) { +func (g *RemoteGenerator) poll(in *securityapi.CertificateRequest) (ca []byte, crt []byte, err error) { timeout := time.NewTimer(time.Minute * 5) defer timeout.Stop() tick := time.NewTicker(time.Second * 5) @@ -92,18 +84,19 @@ func poll(g *Generator, in *securityapi.CertificateRequest, data *userdata.OSSec for { select { case <-timeout.C: - return fmt.Errorf("timeout waiting for certificate") + return nil, nil, fmt.Errorf("timeout waiting for certificate") case <-tick.C: - resp, _err := g.Certificate(in) - if _err != nil { - log.Println(_err) + var resp *securityapi.CertificateResponse + resp, err = g.Certificate(in) + if err != nil { + log.Println(err) continue } - data.CA = &x509.PEMEncodedCertificateAndKey{} - data.CA.Crt = resp.Ca - data.Identity.Crt = resp.Crt - return nil + ca = resp.Ca + crt = resp.Crt + + return ca, crt, nil } } } diff --git a/pkg/grpc/middleware/auth/basic/basic.go b/pkg/grpc/middleware/auth/basic/basic.go index f4648d2e4..b65c79568 100644 --- a/pkg/grpc/middleware/auth/basic/basic.go +++ b/pkg/grpc/middleware/auth/basic/basic.go @@ -6,14 +6,12 @@ package basic import ( "crypto/tls" - "errors" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/talos-systems/talos/pkg/net" - "github.com/talos-systems/talos/pkg/userdata" ) // Credentials describes an authorization method. @@ -43,19 +41,3 @@ func NewConnection(address string, port int, creds credentials.PerRPCCredentials return conn, nil } - -// NewCredentials returns credentials.PerRPCCredentials based on username and -// password, or a token. The token method takes precedence over the username -// and password. -func NewCredentials(data *userdata.Trustd) (creds Credentials, err error) { - switch { - case data.Username != "" && data.Password != "": - creds = NewUsernameAndPasswordCredentials(data.Username, data.Password) - case data.Token != "": - creds = NewTokenCredentials(data.Token) - default: - return nil, errors.New("failed to find valid credentials") - } - - return creds, nil -} diff --git a/pkg/grpc/tls/cert.go b/pkg/grpc/tls/cert.go deleted file mode 100644 index 7a624664d..000000000 --- a/pkg/grpc/tls/cert.go +++ /dev/null @@ -1,185 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package tls - -import ( - "context" - "crypto/tls" - "crypto/x509" - "io/ioutil" - "log" - "sync" - "time" - - "github.com/pkg/errors" - - "github.com/talos-systems/talos/pkg/constants" - "github.com/talos-systems/talos/pkg/grpc/gen" - "github.com/talos-systems/talos/pkg/userdata" -) - -// CertificateProvider describes an interface by which TLS certificates may be managed -type CertificateProvider interface { - - // GetCertificate returns the current certificate matching the given client request - GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) - - // UpdateCertificate updates the stored certificate for the given client request - UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error -} - -type singleCertificateProvider struct { - sync.RWMutex - cert *tls.Certificate - - updateHooks []func(newCert *tls.Certificate) -} - -func (p *singleCertificateProvider) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { - if p == nil { - return nil, errors.New("no provider") - } - - p.RLock() - defer p.RUnlock() - return p.cert, nil -} - -func (p *singleCertificateProvider) UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error { - p.Lock() - p.cert = cert - p.Unlock() - - for _, f := range p.updateHooks { - f(cert) - } - return nil -} - -type userDataCertificateProvider struct { - data *userdata.OSSecurity -} - -func (p *userDataCertificateProvider) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { - cert, err := tls.X509KeyPair(p.data.Identity.Crt, p.data.Identity.Key) - return &cert, err -} - -func (p *userDataCertificateProvider) UpdateCertificate(h *tls.ClientHelloInfo, cert *tls.Certificate) error { - // No-op - return nil -} - -type renewingFileCertificateProvider struct { - singleCertificateProvider - - certFile string - - // For now, this is using the complete userdata object. It should probably - // be pared down to just what is necessary, later. However, the current TLS - // generator code requires the whole thing... so we do, too. - data *userdata.UserData - - g *gen.Generator -} - -// NewRenewingFileCertificateProvider returns a new CertificateProvider which -// manages and updates its certificates from trustd, storing a cache file copy. -// -// TODO: the flow here is a bit wonky, but it should be fixable after we change -// to have the default be ephemeral node certificates. Until then, however, we -// are doing a dance between the userdata-stored cert, the filesystem-cached -// cert, and the memory-cached cert. -func NewRenewingFileCertificateProvider(ctx context.Context, data *userdata.UserData) (CertificateProvider, error) { - g, err := gen.NewGenerator(data, constants.TrustdPort) - if err != nil { - return nil, errors.Wrap(err, "failed to create TLS generator") - } - - p := &renewingFileCertificateProvider{ - g: g, - data: data, - certFile: constants.NodeCertFile, - } - - if err = p.loadInitialCert(); err != nil { - return nil, errors.Wrap(err, "failed to load initial certificate") - } - - go p.manageUpdates(ctx) - - return p, nil -} - -func (p *renewingFileCertificateProvider) loadInitialCert() error { - // TODO: eventually, we will reverse this priority, and have the override - // come from the userdata. For now, however, we use the local file to - // override the userdata, because we _always_ have userdata certs, and the - // userdata is intended to be immutable. - - data, err := ioutil.ReadFile(p.certFile) - if err != nil || len(data) == 0 { - // If we cannot read the cert from the file, then we use the userdata-supplied one - data = p.data.Security.OS.Identity.Crt - } - - cert, err := tls.X509KeyPair(data, p.data.Security.OS.Identity.Key) - if err != nil { - return errors.Wrap(err, "failed to parse cert and key into a TLS Certificate") - } - - return p.UpdateCertificate(nil, &cert) -} - -func (p *renewingFileCertificateProvider) manageUpdates(ctx context.Context) { - nextRenewal := constants.NodeCertRenewalInterval - - for ctx.Err() == nil { - if c, _ := p.GetCertificate(nil); c != nil { // nolint: errcheck - if len(c.Certificate) > 0 { - cert, err := x509.ParseCertificate(c.Certificate[0]) - if err == nil { - nextRenewal = time.Until(cert.NotAfter) / 2 - } else { - log.Println("failed to parse current leaf certificate") - } - } else { - log.Println("current leaf certificate not found") - } - } else { - log.Println("certificate not found") - } - - if nextRenewal > constants.NodeCertRenewalInterval { - nextRenewal = constants.NodeCertRenewalInterval - } - - select { - case <-time.After(nextRenewal): - case <-ctx.Done(): - return - } - if err := p.renewCert(); err != nil { - log.Println("failed to renew certificate:", err) - continue - } - } -} - -func (p *renewingFileCertificateProvider) renewCert() error { - if err := p.g.Identity(p.data); err != nil { - return errors.Wrap(err, "failed to renew certificate") - } - - // TODO: updating the cert using the generator automatically stores the new - // cert to userdata. Therefore, we need to pull that cert out in order to - // update the CertificateProvider's cache of it - cert, err := tls.X509KeyPair(p.data.Security.OS.Identity.Crt, p.data.Security.OS.Identity.Key) - if err != nil { - return errors.Wrap(err, "failed to parse cert and key into a TLS Certificate") - } - - return p.UpdateCertificate(nil, &cert) -} diff --git a/pkg/grpc/tls/local.go b/pkg/grpc/tls/local.go new file mode 100644 index 000000000..f69dd0ee0 --- /dev/null +++ b/pkg/grpc/tls/local.go @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package tls + +import ( + "context" + "crypto/tls" + "net" + + "github.com/pkg/errors" + + "github.com/talos-systems/talos/pkg/crypto/x509" + "github.com/talos-systems/talos/pkg/grpc/gen" +) + +type renewingLocalCertificateProvider struct { + embeddableCertificateProvider + + caKey []byte + caCrt []byte + + generator *gen.LocalGenerator +} + +// NewLocalRenewingFileCertificateProvider returns a new CertificateProvider +// which manages and updates its certificates using a local key. +func NewLocalRenewingFileCertificateProvider(caKey, caCrt []byte, hostname string, ips []net.IP) (CertificateProvider, error) { + g, err := gen.NewLocalGenerator(caKey, caCrt) + if err != nil { + return nil, errors.Wrap(err, "failed to create TLS generator") + } + + provider := &renewingLocalCertificateProvider{ + caKey: caKey, + caCrt: caCrt, + generator: g, + } + + provider.embeddableCertificateProvider = embeddableCertificateProvider{ + hostname: hostname, + ips: ips, + updateFunc: provider.update, + } + + var ( + ca []byte + cert tls.Certificate + ) + if ca, cert, err = provider.updateFunc(); err != nil { + return nil, errors.Wrap(err, "failed to create initial certificate") + } + + if err = provider.UpdateCertificates(ca, &cert); err != nil { + return nil, err + } + + // nolint: errcheck + go provider.manageUpdates(context.Background()) + + return provider, nil +} + +// nolint: dupl +func (p *renewingLocalCertificateProvider) update() (ca []byte, cert tls.Certificate, err error) { + var ( + crt []byte + csr *x509.CertificateSigningRequest + identity *x509.PEMEncodedCertificateAndKey + ) + + csr, identity, err = x509.NewCSRAndIdentity(p.hostname, p.ips) + if err != nil { + return nil, cert, err + } + + if ca, crt, err = p.generator.Identity(csr); err != nil { + return nil, cert, errors.Wrap(err, "failed to generate identity") + } + + identity.Crt = crt + + cert, err = tls.X509KeyPair(identity.Crt, identity.Key) + if err != nil { + return nil, cert, errors.Wrap(err, "failed to parse cert and key into a TLS Certificate") + } + + return ca, cert, nil +} diff --git a/pkg/grpc/tls/provider.go b/pkg/grpc/tls/provider.go new file mode 100644 index 000000000..14026dfbf --- /dev/null +++ b/pkg/grpc/tls/provider.go @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "log" + "net" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/talos-systems/talos/pkg/constants" +) + +// CertificateProvider describes an interface by which TLS certificates may be managed. +type CertificateProvider interface { + // GetCA returns the active root CA. + GetCA() ([]byte, error) + + // GetCertificate returns the current certificate matching the given client request. + GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) + + // UpdateCertificate updates the stored certificate for the given client request. + UpdateCertificates([]byte, *tls.Certificate) error +} + +type embeddableCertificateProvider struct { + sync.RWMutex + + ca []byte + crt *tls.Certificate + + hostname string + ips []net.IP + + updateFunc func() ([]byte, tls.Certificate, error) + updateHooks []func(newCert *tls.Certificate) +} + +func (p *embeddableCertificateProvider) GetCA() ([]byte, error) { + if p == nil { + return nil, errors.New("no provider") + } + + p.RLock() + defer p.RUnlock() + + return p.ca, nil +} + +func (p *embeddableCertificateProvider) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { + if p == nil { + return nil, errors.New("no provider") + } + + p.RLock() + defer p.RUnlock() + + return p.crt, nil +} + +func (p *embeddableCertificateProvider) UpdateCertificates(ca []byte, cert *tls.Certificate) error { + p.Lock() + p.ca = ca + p.crt = cert + p.Unlock() + + for _, f := range p.updateHooks { + f(cert) + } + + return nil +} + +func (p *embeddableCertificateProvider) manageUpdates(ctx context.Context) (err error) { + nextRenewal := constants.DefaultCertificateValidityDuration + + for ctx.Err() == nil { + // nolint: errcheck + if c, _ := p.GetCertificate(nil); c != nil { + if len(c.Certificate) > 0 { + var crt *x509.Certificate + crt, err = x509.ParseCertificate(c.Certificate[0]) + if err == nil { + nextRenewal = time.Until(crt.NotAfter) / 2 + } else { + log.Println("failed to parse current leaf certificate") + } + } else { + log.Println("current leaf certificate not found") + } + } else { + log.Println("certificate not found") + } + + log.Println("next renewal in", nextRenewal) + if nextRenewal > constants.DefaultCertificateValidityDuration { + nextRenewal = constants.DefaultCertificateValidityDuration + } + + select { + case <-time.After(nextRenewal): + case <-ctx.Done(): + return nil + } + + var ( + ca []byte + cert tls.Certificate + ) + if ca, cert, err = p.updateFunc(); err != nil { + log.Println("failed to renew certificate:", err) + continue + } + + if err = p.UpdateCertificates(ca, &cert); err != nil { + log.Println("failed to renew certificate:", err) + continue + } + } + + return errors.New("certificate update manager exited unexpectedly") +} diff --git a/pkg/grpc/tls/remote.go b/pkg/grpc/tls/remote.go new file mode 100644 index 000000000..2c9c77cc8 --- /dev/null +++ b/pkg/grpc/tls/remote.go @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package tls + +import ( + "context" + "crypto/tls" + "net" + + "github.com/pkg/errors" + + "github.com/talos-systems/talos/pkg/crypto/x509" + "github.com/talos-systems/talos/pkg/grpc/gen" +) + +type renewingRemoteCertificateProvider struct { + embeddableCertificateProvider + + generator *gen.RemoteGenerator +} + +// NewRemoteRenewingFileCertificateProvider returns a new CertificateProvider +// which manages and updates its certificates from the security API. +func NewRemoteRenewingFileCertificateProvider(token string, endpoints []string, port int, hostname string, ips []net.IP) (CertificateProvider, error) { + g, err := gen.NewRemoteGenerator(token, endpoints, port) + if err != nil { + return nil, errors.Wrap(err, "failed to create TLS generator") + } + + provider := &renewingRemoteCertificateProvider{ + generator: g, + } + + provider.embeddableCertificateProvider = embeddableCertificateProvider{ + hostname: hostname, + ips: ips, + updateFunc: provider.update, + } + + var ( + ca []byte + cert tls.Certificate + ) + if ca, cert, err = provider.updateFunc(); err != nil { + return nil, errors.Wrap(err, "failed to create initial certificate") + } + + if err = provider.UpdateCertificates(ca, &cert); err != nil { + return nil, err + } + + // nolint: errcheck + go provider.manageUpdates(context.Background()) + + return provider, nil +} + +// nolint: dupl +func (p *renewingRemoteCertificateProvider) update() (ca []byte, cert tls.Certificate, err error) { + var ( + crt []byte + csr *x509.CertificateSigningRequest + identity *x509.PEMEncodedCertificateAndKey + ) + + csr, identity, err = x509.NewCSRAndIdentity(p.hostname, p.ips) + if err != nil { + return nil, cert, err + } + + if ca, crt, err = p.generator.Identity(csr); err != nil { + return nil, cert, errors.Wrap(err, "failed to generate identity") + } + + identity.Crt = crt + + cert, err = tls.X509KeyPair(identity.Crt, identity.Key) + if err != nil { + return nil, cert, errors.Wrap(err, "failed to parse cert and key into a TLS Certificate") + } + + return ca, cert, nil +} diff --git a/pkg/grpc/tls/tls.go b/pkg/grpc/tls/tls.go index 840042c9c..64a277d37 100644 --- a/pkg/grpc/tls/tls.go +++ b/pkg/grpc/tls/tls.go @@ -9,8 +9,6 @@ import ( "crypto/x509" "github.com/pkg/errors" - - "github.com/talos-systems/talos/pkg/userdata" ) // Type represents the TLS authentication type. @@ -47,8 +45,7 @@ func WithClientAuthType(t Type) func(*tls.Config) error { // certificate. // // NOTE: specifying this option will CLEAR any configured Certificates, since -// they would otherwise override this option -// +// they would otherwise override this option. func WithCertificateProvider(p CertificateProvider) func(*tls.Config) error { return func(cfg *tls.Config) error { if p == nil { @@ -101,8 +98,8 @@ func defaultConfig() *tls.Config { } } -// NewConfigWithOpts returns a new TLS Configuration modified by any provided configuration options -func NewConfigWithOpts(opts ...ConfigOptionFunc) (cfg *tls.Config, err error) { +// New returns a new TLS Configuration modified by any provided configuration options +func New(opts ...ConfigOptionFunc) (cfg *tls.Config, err error) { cfg = defaultConfig() for _, f := range opts { @@ -112,20 +109,3 @@ func NewConfigWithOpts(opts ...ConfigOptionFunc) (cfg *tls.Config, err error) { } return } - -// NewConfig initializes a TLS config for the specified type. -func NewConfig(t Type, data *userdata.OSSecurity) (config *tls.Config, err error) { - config = defaultConfig() - - if err = WithClientAuthType(t)(config); err != nil { - return nil, errors.Wrap(err, "failed to apply ClientAuthType preference") - } - if err = WithCACertPEM(data.CA.Crt)(config); err != nil { - return nil, errors.Wrap(err, "failed to apply CA Certificate from UserData") - } - if err = WithCertificateProvider(&userDataCertificateProvider{data: data})(config); err != nil { - return nil, errors.Wrap(err, "failed to apply userdata-sourced CertificateProvider") - } - - return -} diff --git a/pkg/userdata/os_security.go b/pkg/userdata/os_security.go index c8dcf9c6b..3cb66043a 100644 --- a/pkg/userdata/os_security.go +++ b/pkg/userdata/os_security.go @@ -12,8 +12,7 @@ import ( // OSSecurity represents the set of security options specific to the OS. type OSSecurity struct { - CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"` - Identity *x509.PEMEncodedCertificateAndKey `yaml:"identity"` + CA *x509.PEMEncodedCertificateAndKey `yaml:"ca"` } // OSSecurityCheck defines the function type for checks diff --git a/pkg/userdata/userdata.go b/pkg/userdata/userdata.go index 2bba32765..d3acaac28 100644 --- a/pkg/userdata/userdata.go +++ b/pkg/userdata/userdata.go @@ -5,11 +5,9 @@ package userdata import ( - stdlibx509 "crypto/x509" "encoding/pem" "fmt" "io/ioutil" - stdlibnet "net" "os" "strings" @@ -17,7 +15,6 @@ import ( "golang.org/x/xerrors" "github.com/talos-systems/talos/pkg/crypto/x509" - "github.com/talos-systems/talos/pkg/net" yaml "gopkg.in/yaml.v2" ) @@ -90,50 +87,6 @@ type File struct { Path string `yaml:"path"` } -// NewIdentityCSR creates a new CSR for the node's identity certificate. -func (data *UserData) NewIdentityCSR() (csr *x509.CertificateSigningRequest, err error) { - var key *x509.Key - key, err = x509.NewKey() - if err != nil { - return nil, err - } - - data.Security.OS.Identity = &x509.PEMEncodedCertificateAndKey{} - data.Security.OS.Identity.Key = key.KeyPEM - - pemBlock, _ := pem.Decode(key.KeyPEM) - if pemBlock == nil { - return nil, fmt.Errorf("failed to decode key") - } - keyEC, err := stdlibx509.ParseECPrivateKey(pemBlock.Bytes) - if err != nil { - return nil, err - } - ips, err := net.IPAddrs() - if err != nil { - return nil, err - } - for _, san := range data.Services.Trustd.CertSANs { - if ip := stdlibnet.ParseIP(san); ip != nil { - ips = append(ips, ip) - } - } - hostname, err := os.Hostname() - if err != nil { - return - } - opts := []x509.Option{} - names := []string{hostname} - opts = append(opts, x509.DNSNames(names)) - opts = append(opts, x509.IPAddresses(ips)) - csr, err = x509.NewCertificateSigningRequest(keyEC, opts...) - if err != nil { - return nil, err - } - - return csr, nil -} - // Open is a convenience function that reads the user data from disk, and // unmarshals it. func Open(p string) (data *UserData, err error) {