refactor: decouple grpc client and userdata code

This detangles the gRPC client code from the userdata code. The
motivation behind this is to make creating clients more simple and not
dependent on our configuration format.

Signed-off-by: Andrew Rynhard <andrew@andrewrynhard.com>
This commit is contained in:
Andrew Rynhard 2019-09-24 08:32:59 -07:00
parent 607d68008c
commit 6ec5cb02cb
22 changed files with 546 additions and 485 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -64,7 +64,6 @@ func (d *Sequencer) Boot() error {
),
phase.NewPhase(
"user requests",
userdatatask.NewPKITask(),
userdatatask.NewExtraEnvVarsTask(),
userdatatask.NewExtraFilesTask(),
),

View File

@ -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():

View File

@ -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(
&reg.Registrator{
Data: data,
MachineClient: MachineClient,
TimeClient: TimeClient,
NetworkClient: NetworkClient,
MachineClient: machineClient,
TimeClient: timeClient,
NetworkClient: networkClient,
},
factory.Port(constants.OsdPort),
factory.ServerOptions(

View File

@ -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(
&reg.Registrator{Data: data.Security.OS},

View File

@ -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{},

View File

@ -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.

View File

@ -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

35
pkg/grpc/gen/local.go Normal file
View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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)
}

90
pkg/grpc/tls/local.go Normal file
View File

@ -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
}

129
pkg/grpc/tls/provider.go Normal file
View File

@ -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")
}

85
pkg/grpc/tls/remote.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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) {