Merge pull request #50 from justinsb/upup_keys

upup: better secrets support
This commit is contained in:
Justin Santa Barbara 2016-06-04 15:57:08 -04:00
commit c774777160
12 changed files with 733 additions and 47 deletions

View File

@ -275,6 +275,19 @@ func (c *CreateClusterCmd) Run() error {
l.TemplateFunctions["Secrets"] = func() fi.SecretStore {
return secretStore
}
l.TemplateFunctions["GetOrCreateSecret"] = func(id string) (string, error) {
secret, err := secretStore.FindSecret(id)
if err != nil {
return "", fmt.Errorf("error finding secret %q: %v", id, err)
}
if secret == nil {
secret, err = secretStore.CreateSecret(id)
if err != nil {
return "", fmt.Errorf("error creating secret %q: %v", id, err)
}
}
return secret.AsString()
}
if c.SSHPublicKey != "" {
authorized, err := ioutil.ReadFile(c.SSHPublicKey)

67
upup/cmd/upup/pkix.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"bytes"
"crypto/x509/pkix"
"fmt"
"strings"
)
func pkixNameToString(name *pkix.Name) string {
seq := name.ToRDNSequence()
var s bytes.Buffer
for _, rdnSet := range seq {
for _, rdn := range rdnSet {
if s.Len() != 0 {
s.WriteString(",")
}
key := ""
t := rdn.Type
if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 {
switch t[3] {
case 3:
key = "cn"
case 5:
key = "serial"
case 6:
key = "c"
case 7:
key = "l"
case 10:
key = "o"
case 11:
key = "ou"
}
}
if key == "" {
key = t.String()
}
s.WriteString(fmt.Sprintf("%v=%v", key, rdn.Value))
}
}
return s.String()
}
func parsePkixName(s string) (*pkix.Name, error) {
name := new(pkix.Name)
tokens := strings.Split(s, ",")
for _, token := range tokens {
token = strings.TrimSpace(token)
kv := strings.SplitN(token, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("unrecognized token (expected k=v): %q", token)
}
k := strings.ToLower(kv[0])
v := kv[1]
switch k {
case "cn":
name.CommonName = v
default:
return nil, fmt.Errorf("unrecognized key %q in token %q", k, token)
}
}
return name, nil
}

32
upup/cmd/upup/secrets.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
// secretsCmd represents the secrets command
var secretsCmd = &cobra.Command{
Use: "secrets",
Short: "Manage secrets & keys",
Long: `Manage secrets & keys`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("syntax: create describe expose get")
},
}
func init() {
RootCmd.AddCommand(secretsCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// secretsCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// secretsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -0,0 +1,152 @@
package main
import (
"fmt"
"crypto/x509"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kube-deploy/upup/pkg/fi"
"net"
"path"
"strings"
)
type CreateSecretsCommand struct {
StateDir string
Id string
Type string
Usage string
Subject string
AlternateNames []string
}
var createSecretsCommand CreateSecretsCommand
func init() {
cmd := &cobra.Command{
Use: "create",
Short: "Create secrets",
Long: `Create secrets.`,
Run: func(cmd *cobra.Command, args []string) {
err := createSecretsCommand.Run()
if err != nil {
glog.Exitf("%v", err)
}
},
}
secretsCmd.AddCommand(cmd)
cmd.Flags().StringVarP(&createSecretsCommand.StateDir, "state", "", "", "Directory in which to store state")
cmd.Flags().StringVarP(&createSecretsCommand.Type, "type", "", "", "Type of secret to create")
cmd.Flags().StringVarP(&createSecretsCommand.Id, "id", "", "", "Id of secret to create")
cmd.Flags().StringVarP(&createSecretsCommand.Usage, "usage", "", "", "Usage of secret (for SSL certificate)")
cmd.Flags().StringVarP(&createSecretsCommand.Subject, "subject", "", "", "Subject (for SSL certificate)")
cmd.Flags().StringSliceVarP(&createSecretsCommand.AlternateNames, "san", "", nil, "Alternate name (for SSL certificate)")
}
func (cmd *CreateSecretsCommand) Run() error {
if cmd.StateDir == "" {
return fmt.Errorf("state dir is required")
}
if cmd.Id == "" {
return fmt.Errorf("id is required")
}
if cmd.Type == "" {
return fmt.Errorf("type is required")
}
// TODO: Prompt before replacing?
// TODO: Keep history?
switch cmd.Type {
case "secret":
{
secretStore, err := fi.NewFilesystemSecretStore(path.Join(cmd.StateDir, "secrets"))
if err != nil {
return fmt.Errorf("error building secret store: %v", err)
}
_, err = secretStore.CreateSecret(cmd.Id)
if err != nil {
return fmt.Errorf("error creating secrets %v", err)
}
return nil
}
case "keypair":
// TODO: Create a rotate command which keeps the same values?
// Or just do it here a "replace" action - existing=fail, replace or rotate
// TODO: Create a CreateKeypair class, move to fi (this is duplicated code)
{
if cmd.Subject == "" {
return fmt.Errorf("subject is required")
}
subject, err := parsePkixName(cmd.Subject)
if err != nil {
return fmt.Errorf("Error parsing subject: %v", err)
}
template := &x509.Certificate{
Subject: *subject,
BasicConstraintsValid: true,
IsCA: false,
}
if len(template.Subject.ToRDNSequence()) == 0 {
return fmt.Errorf("Subject name was empty")
}
switch cmd.Usage {
case "client":
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature
break
case "server":
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
break
default:
return fmt.Errorf("unknown usage: %q", cmd.Usage)
}
for _, san := range cmd.AlternateNames {
san = strings.TrimSpace(san)
if san == "" {
continue
}
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
}
}
caStore, err := fi.NewFilesystemCAStore(path.Join(cmd.StateDir, "pki"))
if err != nil {
return fmt.Errorf("error building CA store: %v", err)
}
// TODO: Allow resigning of the existing private key?
key, err := caStore.CreatePrivateKey(cmd.Id)
if err != nil {
return fmt.Errorf("error creating privatekey %v", err)
}
_, err = caStore.IssueCert(cmd.Id, key, template)
if err != nil {
return fmt.Errorf("error creating certificate %v", err)
}
return nil
}
default:
return fmt.Errorf("secret type not known: %q", cmd.Type)
}
}

View File

@ -0,0 +1,187 @@
package main
import (
"fmt"
"bytes"
"crypto/rsa"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kube-deploy/upup/pkg/fi"
"os"
"path"
"sort"
"strings"
"text/tabwriter"
)
type DescribeSecretsCommand struct {
StateDir string
}
var describeSecretsCommand DescribeSecretsCommand
func init() {
cmd := &cobra.Command{
Use: "describe",
Short: "Describe secrets",
Long: `Describe secrets.`,
Run: func(cmd *cobra.Command, args []string) {
err := describeSecretsCommand.Run()
if err != nil {
glog.Exitf("%v", err)
}
},
}
secretsCmd.AddCommand(cmd)
cmd.Flags().StringVarP(&describeSecretsCommand.StateDir, "state", "", "", "Directory in which to store state")
}
func (c *DescribeSecretsCommand) Run() error {
if c.StateDir == "" {
return fmt.Errorf("state dir is required")
}
w := new(tabwriter.Writer)
var b bytes.Buffer
// Format in tab-separated columns with a tab stop of 8.
w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.StripEscape)
{
caStore, err := fi.NewFilesystemCAStore(path.Join(c.StateDir, "pki"))
if err != nil {
return fmt.Errorf("error building CA store: %v", err)
}
ids, err := caStore.List()
if err != nil {
return fmt.Errorf("error listing CA store items %v", err)
}
for _, id := range ids {
cert, err := caStore.FindCert(id)
if err != nil {
return fmt.Errorf("error retrieving cert %q: %v", id, err)
}
key, err := caStore.FindPrivateKey(id)
if err != nil {
return fmt.Errorf("error retrieving private key %q: %v", id, err)
}
if key == nil && cert == nil {
continue
}
err = describeKeypair(id, cert, key, &b)
if err != nil {
return err
}
b.WriteString("\n")
_, err = w.Write(b.Bytes())
if err != nil {
return fmt.Errorf("error writing to output: %v", err)
}
b.Reset()
}
}
{
secretStore, err := fi.NewFilesystemSecretStore(path.Join(c.StateDir, "secrets"))
if err != nil {
return fmt.Errorf("error building secret store: %v", err)
}
ids, err := secretStore.ListSecrets()
if err != nil {
return fmt.Errorf("error listing secrets %v", err)
}
for _, id := range ids {
secret, err := secretStore.FindSecret(id)
if err != nil {
return fmt.Errorf("error retrieving secret %q: %v", id, err)
}
if secret == nil {
continue
}
err = describeSecret(id, secret, &b)
if err != nil {
return err
}
b.WriteString("\n")
_, err = w.Write(b.Bytes())
if err != nil {
return fmt.Errorf("error writing to output: %v", err)
}
b.Reset()
}
}
return w.Flush()
}
func describeKeypair(id string, c *fi.Certificate, k *fi.PrivateKey, w *bytes.Buffer) error {
var alternateNames []string
if c != nil {
alternateNames = append(alternateNames, c.Certificate.DNSNames...)
alternateNames = append(alternateNames, c.Certificate.EmailAddresses...)
for _, ip := range c.Certificate.IPAddresses {
alternateNames = append(alternateNames, ip.String())
}
sort.Strings(alternateNames)
}
fmt.Fprintf(w, "Id:\t%s\n", id)
if c != nil && k != nil {
fmt.Fprintf(w, "Type:\t%s\n", "keypair")
} else if c != nil && k == nil {
fmt.Fprintf(w, "Type:\t%s\n", "certificate")
} else if k != nil && c == nil {
// Unexpected!
fmt.Fprintf(w, "Type:\t%s\n", "privatekey")
} else {
return fmt.Errorf("expected either certificate or key to be set")
}
if c != nil {
fmt.Fprintf(w, "Subject:\t%s\n", pkixNameToString(&c.Certificate.Subject))
fmt.Fprintf(w, "Issuer:\t%s\n", pkixNameToString(&c.Certificate.Issuer))
fmt.Fprintf(w, "AlternateNames:\t%s\n", strings.Join(alternateNames, ", "))
fmt.Fprintf(w, "CA:\t%v\n", c.IsCA)
fmt.Fprintf(w, "NotAfter:\t%s\n", c.Certificate.NotAfter)
fmt.Fprintf(w, "NotBefore:\t%s\n", c.Certificate.NotBefore)
// PublicKeyAlgorithm doesn't have a String() function. Also, is this important information?
//fmt.Fprintf(w, "PublicKeyAlgorithm:\t%v\n", c.Certificate.PublicKeyAlgorithm)
//fmt.Fprintf(w, "SignatureAlgorithm:\t%v\n", c.Certificate.SignatureAlgorithm)
}
if k != nil {
if rsaPrivateKey, ok := k.Key.(*rsa.PrivateKey); ok {
fmt.Fprintf(w, "PrivateKeyType:\t%v\n", "rsa")
fmt.Fprintf(w, "KeyLength:\t%v\n", rsaPrivateKey.N.BitLen())
} else {
fmt.Fprintf(w, "PrivateKeyType:\tunknown (%T)\n", k.Key)
}
}
return nil
}
func describeSecret(id string, s *fi.Secret, w *bytes.Buffer) error {
fmt.Fprintf(w, "Id:\t%s\n", id)
fmt.Fprintf(w, "Type:\t%s\n", "secret")
return nil
}

View File

@ -0,0 +1,119 @@
package main
import (
"fmt"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kube-deploy/upup/pkg/fi"
"os"
"path"
)
type ExposeSecretsCommand struct {
StateDir string
ID string
Type string
}
var exposeSecretsCommand ExposeSecretsCommand
func init() {
cmd := &cobra.Command{
Use: "expose",
Short: "Expose secrets",
Long: `Expose secrets.`,
Run: func(cmd *cobra.Command, args []string) {
err := exposeSecretsCommand.Run()
if err != nil {
glog.Exitf("%v", err)
}
},
}
secretsCmd.AddCommand(cmd)
cmd.Flags().StringVarP(&exposeSecretsCommand.StateDir, "state", "", "", "Directory in which to store state")
cmd.Flags().StringVarP(&exposeSecretsCommand.Type, "type", "", "", "Type of secret to create")
cmd.Flags().StringVarP(&exposeSecretsCommand.ID, "id", "", "", "Id of secret to create")
}
func (cmd *ExposeSecretsCommand) Run() error {
if cmd.StateDir == "" {
return fmt.Errorf("state dir is required")
}
id := cmd.ID
if id == "" {
return fmt.Errorf("id is required")
}
if cmd.Type == "" {
return fmt.Errorf("type is required")
}
var value string
switch cmd.Type {
case "secret":
{
secretStore, err := fi.NewFilesystemSecretStore(path.Join(cmd.StateDir, "secrets"))
if err != nil {
return fmt.Errorf("error building secret store: %v", err)
}
secret, err := secretStore.FindSecret(id)
if err != nil {
return fmt.Errorf("error finding secret %q: %v", id, err)
}
if secret == nil {
return fmt.Errorf("secret not found: %q", id)
}
value = string(secret.Data)
}
case "certificate", "privatekey":
{
caStore, err := fi.NewFilesystemCAStore(path.Join(cmd.StateDir, "pki"))
if err != nil {
return fmt.Errorf("error building CA store: %v", err)
}
if cmd.Type == "privatekey" {
k, err := caStore.FindPrivateKey(id)
if err != nil {
return fmt.Errorf("error finding privatekey: %v", err)
}
if k == nil {
return fmt.Errorf("privatekey not found: %q", id)
}
value, err = k.AsString()
if err != nil {
return fmt.Errorf("error encoding privatekey: %v", err)
}
} else {
c, err := caStore.FindCert(id)
if err != nil {
return fmt.Errorf("error finding certificate: %v", err)
}
if c == nil {
return fmt.Errorf("certificate not found: %q", id)
}
value, err = c.AsString()
if err != nil {
return fmt.Errorf("error encoding certiifcate: %v", err)
}
}
}
default:
return fmt.Errorf("secret type not known: %q", cmd.Type)
}
_, err := fmt.Fprint(os.Stdout, value)
if err != nil {
return fmt.Errorf("error writing to output: %v", err)
}
return nil
}

View File

@ -0,0 +1,111 @@
package main
import (
"fmt"
"bytes"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kube-deploy/upup/pkg/fi"
"os"
"path"
"text/tabwriter"
)
type GetSecretsCommand struct {
StateDir string
}
var getSecretsCommand GetSecretsCommand
func init() {
cmd := &cobra.Command{
Use: "get",
Short: "Get secrets",
Long: `Get secrets.`,
Run: func(cmd *cobra.Command, args []string) {
err := getSecretsCommand.Run()
if err != nil {
glog.Exitf("%v", err)
}
},
}
secretsCmd.AddCommand(cmd)
cmd.Flags().StringVarP(&getSecretsCommand.StateDir, "state", "", "", "Directory in which to store state")
}
type SecretInfo struct {
Id string
Type string
}
func (c *GetSecretsCommand) Run() error {
if c.StateDir == "" {
return fmt.Errorf("state dir is required")
}
var infos []*SecretInfo
{
caStore, err := fi.NewFilesystemCAStore(path.Join(c.StateDir, "pki"))
if err != nil {
return fmt.Errorf("error building CA store: %v", err)
}
ids, err := caStore.List()
if err != nil {
return fmt.Errorf("error listing CA store items %v", err)
}
for _, id := range ids {
info := &SecretInfo{
Id: id,
Type: "keypair",
}
infos = append(infos, info)
}
}
{
secretStore, err := fi.NewFilesystemSecretStore(path.Join(c.StateDir, "secrets"))
if err != nil {
return fmt.Errorf("error building secret store: %v", err)
}
ids, err := secretStore.ListSecrets()
if err != nil {
return fmt.Errorf("error listing secrets %v", err)
}
for _, id := range ids {
info := &SecretInfo{
Id: id,
Type: "secret",
}
infos = append(infos, info)
}
}
var b bytes.Buffer
w := new(tabwriter.Writer)
// Format in tab-separated columns with a tab stop of 8.
w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.StripEscape)
for _, info := range infos {
b.WriteByte(tabwriter.Escape)
b.WriteString(info.Type)
b.WriteByte(tabwriter.Escape)
b.WriteByte('\t')
b.WriteByte(tabwriter.Escape)
b.WriteString(info.Id)
b.WriteByte(tabwriter.Escape)
b.WriteByte('\n')
_, err := w.Write(b.Bytes())
if err != nil {
return fmt.Errorf("error writing to output: %v", err)
}
b.Reset()
}
w.Flush()
return nil
}

View File

@ -12,17 +12,17 @@ APIServer:
Key: {{ Base64Encode (CA.PrivateKey "master").AsString }}
KubeUser: {{ .KubeUser }}
KubePassword: {{ (Secrets.Secret "kube").AsString }}
KubePassword: {{ GetOrCreateSecret "kube" }}
Tokens:
admin: {{ (Secrets.Secret "admin").AsString }}
kubelet: {{ (Secrets.Secret "kubelet").AsString }}
kube-proxy: {{ (Secrets.Secret "kube-proxy").AsString }}
"system:scheduler": {{ (Secrets.Secret "system:scheduler").AsString }}
"system:controller_manager": {{ (Secrets.Secret "system:controller_manager").AsString }}
"system:logging": {{ (Secrets.Secret "system:logging").AsString }}
"system:monitoring": {{ (Secrets.Secret "system:monitoring").AsString }}
"system:dns": {{ (Secrets.Secret "system:dns").AsString }}
admin: {{ GetOrCreateSecret "admin" }}
kubelet: {{ GetOrCreateSecret "kubelet" }}
kube-proxy: {{ GetOrCreateSecret "kube-proxy" }}
"system:scheduler": {{ GetOrCreateSecret "system:scheduler" }}
"system:controller_manager": {{ GetOrCreateSecret "system:controller_manager" }}
"system:logging": {{ GetOrCreateSecret "system:logging" }}
"system:monitoring": {{ GetOrCreateSecret "system:monitoring" }}
"system:dns": {{ GetOrCreateSecret "system:dns" }}
Tags:
{{ range $tag := Args }}

View File

@ -63,6 +63,8 @@ type CAStore interface {
IssueCert(id string, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error)
CreatePrivateKey(id string) (*PrivateKey, error)
List() ([]string, error)
}
func (c *Certificate) AsString() (string, error) {

View File

@ -11,6 +11,7 @@ import (
"io/ioutil"
"os"
"path"
"strings"
)
type FilesystemCAStore struct {
@ -106,41 +107,6 @@ func (c *FilesystemCAStore) generateCACertificate() error {
return nil
}
func (c *FilesystemCAStore) getSubjectKey(subject *pkix.Name) string {
seq := subject.ToRDNSequence()
var s bytes.Buffer
for _, rdnSet := range seq {
for _, rdn := range rdnSet {
if s.Len() != 0 {
s.WriteString(",")
}
key := ""
t := rdn.Type
if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 {
switch t[3] {
case 3:
key = "cn"
case 5:
key = "serial"
case 6:
key = "c"
case 7:
key = "l"
case 10:
key = "o"
case 11:
key = "ou"
}
}
if key == "" {
key = t.String()
}
s.WriteString(fmt.Sprintf("%v=%v", key, rdn.Value))
}
}
return s.String()
}
func (c *FilesystemCAStore) buildCertificatePath(id string) string {
return path.Join(c.basedir, "issued", id+".crt")
}
@ -190,6 +156,26 @@ func (c *FilesystemCAStore) FindCert(id string) (*Certificate, error) {
return cert, nil
}
func (c *FilesystemCAStore) List() ([]string, error) {
var ids []string
if c.caCertificate != nil {
ids = append(ids, "ca")
}
issuedDir := path.Join(c.basedir, "issued")
files, err := ioutil.ReadDir(issuedDir)
if err != nil {
return nil, fmt.Errorf("error reading directory %q: %v", issuedDir, err)
}
for _, f := range files {
name := f.Name()
name = strings.TrimSuffix(name, ".crt")
ids = append(ids, name)
}
return ids, nil
}
func (c *FilesystemCAStore) IssueCert(id string, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error) {
p := c.buildCertificatePath(id)

View File

@ -38,15 +38,26 @@ func (c *FilesystemSecretStore) FindSecret(id string) (*Secret, error) {
return s, nil
}
func (c *FilesystemSecretStore) ListSecrets() ([]string, error) {
files, err := ioutil.ReadDir(c.basedir)
if err != nil {
return nil, fmt.Errorf("error listing secrets directory: %v", err)
}
var ids []string
for _, f := range files {
id := f.Name()
ids = append(ids, id)
}
return ids, nil
}
func (c *FilesystemSecretStore) Secret(id string) (*Secret, error) {
s, err := c.FindSecret(id)
if err != nil {
return nil, err
}
if s == nil {
// For now, we auto-create the secret
return c.CreateSecret(id)
// return nil, fmt.Errorf("Secret not found: %q", id)
return nil, fmt.Errorf("Secret not found: %q", id)
}
return s, nil
}

View File

@ -8,8 +8,14 @@ import (
)
type SecretStore interface {
// Get a secret. Returns an error if not found
Secret(id string) (*Secret, error)
// Find a secret, if exists. Returns nil,nil if not found
FindSecret(id string) (*Secret, error)
// Create or replace a secret
CreateSecret(id string) (*Secret, error)
// Lists the ids of all known secrets
ListSecrets() ([]string, error)
}
type Secret struct {