feat: add ability to generate userdata secrets (#581)

Signed-off-by: Andrew Rynhard <andrew@andrewrynhard.com>
This commit is contained in:
Andrew Rynhard 2019-04-26 20:56:40 -07:00 committed by GitHub
parent 2a4b56d4a1
commit 20662217a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 284 additions and 276 deletions

2
go.sum
View File

@ -157,8 +157,6 @@ golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@ -0,0 +1,223 @@
/* 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 (
"bytes"
stdlibx509 "crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"math/rand"
"net"
"text/template"
"github.com/talos-systems/talos/pkg/crypto/x509"
)
// 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
KubeadmTokens *KubeadmTokens
TrustdInfo *TrustdInfo
}
// 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 {
Username string
Password string
}
// RandomString returns a string of length n.
func RandomString(n int) string {
var letter = []rune("abcdefghijklmnopqrstuvwxy0123456789")
b := make([]rune, n)
for i := range b {
b[i] = letter[rand.Intn(len(letter))]
}
return string(b)
}
// NewInput generates the sensitive data required to generate all userdata
// types.
// nolint: gocyclo
func NewInput(clustername string, masterIPs []string) (input *Input, err error) {
kubeadmTokens := &KubeadmTokens{
BootstrapToken: RandomString(6) + "." + RandomString(16),
CertKey: RandomString(26),
}
trustdInfo := &TrustdInfo{
Username: RandomString(14),
Password: RandomString(24),
}
// 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
}
// 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,
KubeadmTokens: kubeadmTokens,
TrustdInfo: trustdInfo,
}
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")
}
ud, err := renderTemplate(in, template)
if err != nil {
return "", err
}
return ud, nil
}
// renderTemplate will output a templated string.
func renderTemplate(in *Input, udTemplate string) (string, error) {
templ := template.Must(template.New("udTemplate").Parse(udTemplate))
var buf bytes.Buffer
if err := templ.Execute(&buf, in); err != nil {
return "", err
}
return buf.String(), nil
}

View File

@ -0,0 +1,61 @@
/* 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_test
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/talos/pkg/userdata"
"github.com/talos-systems/talos/pkg/userdata/generate"
"gopkg.in/yaml.v2"
)
var (
input *generate.Input
)
type GenerateSuite struct {
suite.Suite
}
func TestGenerateSuite(t *testing.T) {
suite.Run(t, new(GenerateSuite))
}
func (suite *GenerateSuite) SetupSuite() {
var err error
input, err = generate.NewInput("test", []string{"10.0.1.5", "10.0.1.6", "10.0.1.7"})
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateInitSuccess() {
dataString, err := generate.Userdata(generate.TypeInit, input)
suite.Require().NoError(err)
data := &userdata.UserData{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateControlPlaneSuccess() {
dataString, err := generate.Userdata(generate.TypeControlPlane, input)
suite.Require().NoError(err)
data := &userdata.UserData{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateWorkerSuccess() {
dataString, err := generate.Userdata(generate.TypeJoin, input)
suite.Require().NoError(err)
data := &userdata.UserData{}
err = yaml.Unmarshal([]byte(dataString), data)
suite.Require().NoError(err)
}
func (suite *GenerateSuite) TestGenerateTalosconfigSuccess() {
_, err := generate.Talosconfig(input)
suite.Require().NoError(err)
}

View File

@ -1,85 +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 generate
import (
"bytes"
"errors"
"text/template"
)
// 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 {
Type string // Valid values are init, controlplane, or worker.
Certs *Certs
MasterIPs []string
Index int
ClusterName string
ServiceDomain string
PodNet []string
ServiceNet []string
Endpoints string
KubeadmTokens *KubeadmTokens
TrustdInfo *TrustdInfo
}
// 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 {
Username string
Password string
}
// Userdata will return the talos userdata for a given node type.
func Userdata(in *Input) (string, error) {
templateData := ""
switch udtype := in.Type; udtype {
case "init":
templateData = initTempl
case "controlplane":
templateData = controlPlaneTempl
case "worker":
templateData = workerTempl
default:
return "", errors.New("unable to determine userdata type to generate")
}
ud, err := renderTemplate(in, templateData)
if err != nil {
return "", err
}
return ud, nil
}
// renderTemplate will output a templated string.
func renderTemplate(in *Input, udTemplate string) (string, error) {
templ := template.Must(template.New("udTemplate").Parse(udTemplate))
var buf bytes.Buffer
if err := templ.Execute(&buf, in); err != nil {
return "", err
}
return buf.String(), nil
}

View File

@ -1,189 +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 generate_test
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/talos/pkg/userdata/generate"
)
const (
expectedInitConfig = `---
version: ""
security:
os:
ca:
crt: ""
key: ""
kubernetes:
ca:
crt: ""
key: ""
services:
init:
cni: flannel
kubeadm:
certificateKey: 'testcrtkey'
configuration: |
apiVersion: kubeadm.k8s.io/v1beta1
kind: InitConfiguration
apiEndpoint:
advertiseAddress: 10.0.1.5
bindPort: 6443
bootstrapTokens:
- token: 'abcdef.1234567890123456789'
ttl: 0s
nodeRegistration:
taints: []
kubeletExtraArgs:
node-labels: ""
feature-gates: ExperimentalCriticalPodAnnotation=true
---
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
clusterName: test
controlPlaneEndpoint: 10.0.1.5
apiServer:
certSANs: [ "10.0.1.5","10.0.1.6","10.0.1.7" ]
extraArgs:
runtime-config: settings.k8s.io/v1alpha1=true
feature-gates: ExperimentalCriticalPodAnnotation=true
controllerManager:
extraArgs:
terminated-pod-gc-threshold: '100'
feature-gates: ExperimentalCriticalPodAnnotation=true
scheduler:
extraArgs:
feature-gates: ExperimentalCriticalPodAnnotation=true
networking:
dnsDomain: cluster.local
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: lc
trustd:
username: 'test'
password: 'test'
endpoints: [ ]
certSANs: [ "10.0.1.5" ]
`
expectedControlPlaneConfig = `---
version: ""
security: null
services:
init:
cni: flannel
kubeadm:
certificateKey: 'testcrtkey'
configuration: |
apiVersion: kubeadm.k8s.io/v1beta1
kind: JoinConfiguration
controlPlane:
apiEndpoint:
advertiseAddress: 10.0.1.5
bindPort: 6443
discovery:
bootstrapToken:
token: 'abcdef.1234567890123456789'
unsafeSkipCAVerification: true
apiServerEndpoint: 10.0.1.5:443
nodeRegistration:
taints: []
kubeletExtraArgs:
node-labels: ""
feature-gates: ExperimentalCriticalPodAnnotation=true
trustd:
username: 'test'
password: 'test'
endpoints: [ "10.0.1.5" ]
bootstrapNode: "10.0.1.5"
certSANs: [ "10.0.1.5" ]
`
expectedWorkerConfig = `---
version: ""
security: null
services:
init:
cni: flannel
kubeadm:
configuration: |
apiVersion: kubeadm.k8s.io/v1beta1
kind: JoinConfiguration
discovery:
bootstrapToken:
token: 'abcdef.1234567890123456789'
unsafeSkipCAVerification: true
apiServerEndpoint: 10.0.1.5:443
nodeRegistration:
taints: []
kubeletExtraArgs:
node-labels: ""
feature-gates: ExperimentalCriticalPodAnnotation=true
token: 'abcdef.1234567890123456789'
trustd:
username: 'test'
password: 'test'
endpoints: [ "10.0.1.5","10.0.1.6","10.0.1.7" ]
`
)
var (
input = generate.Input{
Certs: &generate.Certs{},
MasterIPs: []string{"10.0.1.5", "10.0.1.6", "10.0.1.7"},
PodNet: []string{"10.244.0.0/16"},
ServiceNet: []string{"10.96.0.0/12"},
ServiceDomain: "cluster.local",
ClusterName: "test",
KubeadmTokens: &generate.KubeadmTokens{
BootstrapToken: "abcdef.1234567890123456789",
CertKey: "testcrtkey",
},
TrustdInfo: &generate.TrustdInfo{
Username: "test",
Password: "test",
},
}
)
type GenerateSuite struct {
suite.Suite
}
func TestGenerateSuite(t *testing.T) {
suite.Run(t, new(GenerateSuite))
}
func (suite *GenerateSuite) TestGenerateInitSuccess() {
i := input
i.Type = "init"
userdata, err := generate.Userdata(&i)
suite.Require().NoError(err)
suite.Assert().Equal(userdata, expectedInitConfig)
}
func (suite *GenerateSuite) TestGenerateControlPlaneSuccess() {
i := input
i.Type = "controlplane"
userdata, err := generate.Userdata(&i)
suite.Require().NoError(err)
suite.Assert().Equal(userdata, expectedControlPlaneConfig)
}
func (suite *GenerateSuite) TestGenerateWorkerSuccess() {
i := input
i.Type = "worker"
userdata, err := generate.Userdata(&i)
suite.Require().NoError(err)
suite.Assert().Equal(userdata, expectedWorkerConfig)
}