talos/pkg/userdata/generate/generate.go
Spencer Smith 18f59d8f0b
fix: move to crypto/rand for token gen (#794)
Signed-off-by: Spencer Smith <robertspencersmith@gmail.com>
2019-06-27 18:08:39 -04:00

329 lines
8.3 KiB
Go

/* 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 generate
import (
"bufio"
"bytes"
"crypto/rand"
stdlibx509 "crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"net"
"strings"
"text/template"
"github.com/talos-systems/talos/internal/pkg/constants"
"github.com/talos-systems/talos/pkg/crypto/x509"
"github.com/talos-systems/talos/pkg/userdata/token"
)
// CertStrings holds the string representation of a certificate and key.
type CertStrings struct {
Crt string
Key string
}
// Input holds info about certs, ips, and node type.
type Input struct {
Certs *Certs
MasterIPs []string
Index int
ClusterName string
ServiceDomain string
PodNet []string
ServiceNet []string
Endpoints string
KubernetesVersion string
KubeadmTokens *KubeadmTokens
TrustdInfo *TrustdInfo
InitToken *token.Token
IP net.IP
}
// Certs holds the base64 encoded keys and certificates.
type Certs struct {
AdminCert string
AdminKey string
OsCert string
OsKey string
K8sCert string
K8sKey string
}
// KubeadmTokens holds the senesitve kubeadm data.
type KubeadmTokens struct {
BootstrapToken string
CertKey string
}
// TrustdInfo holds the trustd credentials.
type TrustdInfo struct {
Token string
}
// randBytes returns a random string consisting of the characters in
// validBootstrapTokenChars, with the length customized by the parameter
func randBytes(length int) (string, error) {
// validBootstrapTokenChars defines the characters a bootstrap token can consist of
const validBootstrapTokenChars = "0123456789abcdefghijklmnopqrstuvwxyz"
// len("0123456789abcdefghijklmnopqrstuvwxyz") = 36 which doesn't evenly divide
// the possible values of a byte: 256 mod 36 = 4. Discard any random bytes we
// read that are >= 252 so the bytes we evenly divide the character set.
const maxByteValue = 252
var (
b byte
err error
token = make([]byte, length)
)
reader := bufio.NewReaderSize(rand.Reader, length*2)
for i := range token {
for {
if b, err = reader.ReadByte(); err != nil {
return "", err
}
if b < maxByteValue {
break
}
}
token[i] = validBootstrapTokenChars[int(b)%len(validBootstrapTokenChars)]
}
return string(token), err
}
//genToken will generate a token of the format abc.123 (like kubeadm/trustd), where the length of the first string (before the dot)
//and length of the second string (after dot) are specified as inputs
func genToken(lenFirst int, lenSecond int) (string, error) {
var err error
var tokenTemp = make([]string, 2)
tokenTemp[0], err = randBytes(lenFirst)
if err != nil {
return "", err
}
tokenTemp[1], err = randBytes(lenSecond)
if err != nil {
return "", err
}
return tokenTemp[0] + "." + tokenTemp[1], nil
}
// NewInput generates the sensitive data required to generate all userdata
// types.
// nolint: gocyclo
func NewInput(clustername string, masterIPs []string) (input *Input, err error) {
//Gen trustd token strings
kubeadmBootstrapToken, err := genToken(6, 16)
if err != nil {
return nil, err
}
//TODO: Can be dropped
//Gen kubeadm cert key
kubeadmCertKey, err := randBytes(26)
if err != nil {
return nil, err
}
//Gen trustd token strings
trustdToken, err := genToken(6, 16)
if err != nil {
return nil, err
}
kubeadmTokens := &KubeadmTokens{
BootstrapToken: kubeadmBootstrapToken,
CertKey: kubeadmCertKey,
}
trustdInfo := &TrustdInfo{
Token: trustdToken,
}
// Generate Kubernetes CA.
opts := []x509.Option{x509.RSA(true), x509.Organization("talos-k8s")}
k8sCert, err := x509.NewSelfSignedCertificateAuthority(opts...)
if err != nil {
return nil, err
}
// Generate Talos CA.
opts = []x509.Option{x509.RSA(false), x509.Organization("talos-os")}
osCert, err := x509.NewSelfSignedCertificateAuthority(opts...)
if err != nil {
return nil, err
}
// Create the init token
tok, err := token.NewToken()
if err != nil {
return nil, err
}
// Generate the admin talosconfig.
adminKey, err := x509.NewKey()
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(adminKey.KeyPEM)
if pemBlock == nil {
return nil, errors.New("failed to decode admin key pem")
}
adminKeyEC, err := stdlibx509.ParseECPrivateKey(pemBlock.Bytes)
if err != nil {
return nil, err
}
ips := []net.IP{net.ParseIP("127.0.0.1")}
opts = []x509.Option{x509.IPAddresses(ips)}
csr, err := x509.NewCertificateSigningRequest(adminKeyEC, opts...)
if err != nil {
return nil, err
}
csrPemBlock, _ := pem.Decode(csr.X509CertificateRequestPEM)
if csrPemBlock == nil {
return nil, errors.New("failed to decode csr pem")
}
ccsr, err := stdlibx509.ParseCertificateRequest(csrPemBlock.Bytes)
if err != nil {
return nil, err
}
caPemBlock, _ := pem.Decode(osCert.CrtPEM)
if caPemBlock == nil {
return nil, errors.New("failed to decode ca cert pem")
}
caCrt, err := stdlibx509.ParseCertificate(caPemBlock.Bytes)
if err != nil {
return nil, err
}
caKeyPemBlock, _ := pem.Decode(osCert.KeyPEM)
if caKeyPemBlock == nil {
return nil, errors.New("failed to decode ca key pem")
}
caKey, err := stdlibx509.ParseECPrivateKey(caKeyPemBlock.Bytes)
if err != nil {
return nil, err
}
adminCrt, err := x509.NewCertificateFromCSR(caCrt, caKey, ccsr)
if err != nil {
return nil, err
}
certs := &Certs{
AdminCert: base64.StdEncoding.EncodeToString(adminCrt.X509CertificatePEM),
AdminKey: base64.StdEncoding.EncodeToString(adminKey.KeyPEM),
OsCert: base64.StdEncoding.EncodeToString(osCert.CrtPEM),
OsKey: base64.StdEncoding.EncodeToString(osCert.KeyPEM),
K8sCert: base64.StdEncoding.EncodeToString(k8sCert.CrtPEM),
K8sKey: base64.StdEncoding.EncodeToString(k8sCert.KeyPEM),
}
input = &Input{
Certs: certs,
MasterIPs: masterIPs,
PodNet: []string{"10.244.0.0/16"},
ServiceNet: []string{"10.96.0.0/12"},
ServiceDomain: "cluster.local",
ClusterName: clustername,
Endpoints: strings.Join(masterIPs, ", "),
KubernetesVersion: constants.KubernetesVersion,
KubeadmTokens: kubeadmTokens,
TrustdInfo: trustdInfo,
InitToken: tok,
}
return input, nil
}
// Type represents a userdata type.
type Type int
const (
// TypeInit indicates a userdata type should correspond to the kubeadm
// InitConfiguration type.
TypeInit Type = iota
// TypeControlPlane indicates a userdata type should correspond to the
// kubeadm JoinConfiguration type that has the ControlPlane field
// defined.
TypeControlPlane
// TypeJoin indicates a userdata type should correspond to the kubeadm
// JoinConfiguration type.
TypeJoin
)
// Sring returns the string representation of Type.
func (t Type) String() string {
return [...]string{"Init", "ControlPlane", "Join"}[t]
}
// Userdata returns the talos userdata for a given node type.
func Userdata(t Type, in *Input) (string, error) {
var template string
switch t {
case TypeInit:
template = initTempl
case TypeControlPlane:
template = controlPlaneTempl
case TypeJoin:
template = workerTempl
default:
return "", errors.New("failed to determine userdata type to generate")
}
var err error
var ud string
ud, err = renderTemplate(in, template)
if err != nil {
return "", err
}
// TODO: We cant implement this currently because of
// issues with kubeadm dependency mismatch between
// talos and clusterapi//kubebuilder.
// We should figure out way we can work around/through
// this
/*
// Create an actual userdata struct from the
// generated data so we can call validate
// and ensure we are providing proper data
data := &userdata.UserData{}
if err = yaml.Unmarshal([]byte(ud), data); err != nil {
return "", err
}
if err = data.Validate(); err != nil {
return "", err
}
*/
return ud, nil
}
// renderTemplate will output a templated string.
func renderTemplate(in *Input, udTemplate string) (string, error) {
// So we can have a simple add func
funcs := template.FuncMap{"add": add}
templ := template.Must(template.New("udTemplate").Funcs(funcs).Parse(udTemplate))
var buf bytes.Buffer
if err := templ.Execute(&buf, in); err != nil {
return "", err
}
return buf.String(), nil
}
func add(a, b int) int {
return a + b
}