Work in progress implementation of PKI with helm support

This commit is contained in:
Mariano Cano 2021-08-04 20:15:26 -07:00
parent 798b90c359
commit 50f7a0d0c0
2 changed files with 369 additions and 138 deletions

150
pki/helm.go Normal file
View file

@ -0,0 +1,150 @@
package pki
import (
"io"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
authconfig "github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/linkedca"
)
type helmVariables struct {
linkedca.Configuration
Defaults linkedca.Defaults
Password string
SSH struct {
Enabled bool
}
TLS authconfig.TLSOptions
Provisioners []provisioner.Interface
}
func (p *PKI) WriteHelmTemplate(w io.Writer) error {
tmpl, err := template.New("helm").Funcs(sprig.TxtFuncMap()).Parse(helmTemplate)
if err != nil {
return errors.Wrap(err, "error writing helm template")
}
// Delete ssh section if it is not enabled
if !p.options.enableSSH {
p.Ssh = nil
}
if err := tmpl.Execute(w, helmVariables{
Configuration: p.Configuration,
Defaults: p.Defaults,
Password: "asdf",
TLS: authconfig.DefaultTLSOptions,
Provisioners: []provisioner.Interface{
&provisioner.JWK{
Name: p.options.provisioner,
Type: "JWK",
Key: p.ottPublicKey,
EncryptedKey: "",
},
},
}); err != nil {
return errors.Wrap(err, "error executing helm template")
}
return nil
}
const helmTemplate = `# Helm template
inject:
enabled: true
# Config contains the configuration files ca.json and defaults.json
config:
files:
ca.json:
root: {{ first .Root }}
federateRoots: []
crt: {{ .Intermediate }}
key: {{ .IntermediateKey }}
{{- if .SSH.Enabled }}
ssh:
hostKey: {{ .Ssh.HostKey }}
userKey: {{ .Ssh.UserKey }}
{{- end }}
address: {{ .Address }}
dnsNames:
{{- range .DnsNames }}
- {{ . }}
{{- end }}
logger:
format: json
db:
type: badger
dataSource: /home/step/db
authority:
provisioners:
{{- range .Provisioners }}
- {{ . | toJson }}
{{- end }}
tls:
cipherSuites:
{{- range .TLS.CipherSuites }}
- {{ . }}
{{- end }}
minVersion: {{ .TLS.MinVersion }}
maxVersion: {{ .TLS.MaxVersion }}
renegotiation: {{ .TLS.Renegotiation }}
defaults.json:
ca-url: {{ .Defaults.CaUrl }}
ca-config: {{ .Defaults.CaConfig }}
fingerprint: {{ .Defaults.Fingerprint }}
root: {{ .Defaults.Root }}
# Certificates contains the root and intermediate certificate and
# optionally the SSH host and user public keys
certificates:
# intermediate_ca contains the text of the intermediate CA Certificate
intermediate_ca: |
{{- index .Files .Intermediate | toString | nindent 6 }}
# root_ca contains the text of the root CA Certificate
root_ca: |
{{- first .Root | index .Files | toString | nindent 6 }}
{{- if .Ssh }}
# ssh_host_ca contains the text of the public ssh key for the SSH root CA
ssh_host_ca: {{ index .Files .Ssh.HostPublicKey | toString }}
# ssh_user_ca contains the text of the public ssh key for the SSH root CA
ssh_user_ca: {{ index .Files .Ssh.UserPublicKey | toString }}
{{- end }}
# Secrets contains the root and intermediate keys and optionally the SSH
# private keys
secrets:
# ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key
# This value must be base64 encoded.
ca_password: {{ .Password | b64enc }}
provisioner_password: {{ .Password | b64enc}}
x509:
# intermediate_ca_key contains the contents of your encrypted intermediate CA key
intermediate_ca_key: |
{{- index .Files .IntermediateKey | toString | nindent 8 }}
# root_ca_key contains the contents of your encrypted root CA key
# Note that this value can be omitted without impacting the functionality of step-certificates
# If supplied, this should be encrypted using a unique password that is not used for encrypting
# the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.
root_ca_key: |
{{- first .RootKey | index .Files | toString | nindent 8 }}
{{- if .Ssh }}
ssh:
# ssh_host_ca_key contains the contents of your encrypted SSH Host CA key
host_ca_key: |
{{- index .Files .Ssh.HostKey | toString | nindent 8 }}
# ssh_user_ca_key contains the contents of your encrypted SSH User CA key
user_ca_key: |
{{- index .Files .Ssh.UserKey | toString | nindent 8 }}
{{- end }}
`

View file

@ -156,96 +156,108 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
}
type options struct {
address string
caURL string
dnsNames []string
// address string
// caURL string
// dnsNames []string
provisioner string
enableACME bool
enableSSH bool
enableAdmin bool
noDB bool
isHelm bool
deploymentType DeploymentType
}
// PKIOption is the type of a configuration option on the pki constructor.
type PKIOption func(o *options)
type PKIOption func(p *PKI)
// WithAddress sets the listen address of step-ca.
func WithAddress(s string) PKIOption {
return func(o *options) {
o.address = s
return func(p *PKI) {
p.Address = s
}
}
// WithCaUrl sets the default ca-url of step-ca.
func WithCaUrl(s string) PKIOption {
return func(o *options) {
o.caURL = s
return func(p *PKI) {
p.Defaults.CaUrl = s
}
}
// WithDNSNames sets the SANs of step-ca.
func WithDNSNames(s []string) PKIOption {
return func(o *options) {
o.dnsNames = s
return func(p *PKI) {
p.DnsNames = s
}
}
// WithProvisioner defines the name of the default provisioner.
func WithProvisioner(s string) PKIOption {
return func(o *options) {
o.provisioner = s
return func(p *PKI) {
p.options.provisioner = s
}
}
// WithACME enables acme provisioner in step-ca.
func WithACME() PKIOption {
return func(o *options) {
o.enableACME = true
return func(p *PKI) {
p.options.enableACME = true
}
}
// WithSSH enables ssh in step-ca.
func WithSSH() PKIOption {
return func(o *options) {
o.enableSSH = true
return func(p *PKI) {
p.options.enableSSH = true
}
}
// WithAdmin enables the admin api in step-ca.
func WithAdmin() PKIOption {
return func(o *options) {
o.enableAdmin = true
return func(p *PKI) {
p.options.enableAdmin = true
}
}
// WithNoDB disables the db in step-ca.
func WithNoDB() PKIOption {
return func(o *options) {
o.noDB = true
return func(p *PKI) {
p.options.noDB = true
}
}
// WithHelm configures the pki to create a helm values.yaml.
func WithHelm() PKIOption {
return func(p *PKI) {
p.options.isHelm = true
}
}
// WithDeploymentType defines the deployment type of step-ca.
func WithDeploymentType(dt DeploymentType) PKIOption {
return func(o *options) {
o.deploymentType = dt
return func(p *PKI) {
p.options.deploymentType = dt
}
}
// PKI represents the Public Key Infrastructure used by a certificate authority.
type PKI struct {
casOptions apiv1.Options
caService apiv1.CertificateAuthorityService
caCreator apiv1.CertificateAuthorityCreator
root, rootKey, rootFingerprint string
intermediate, intermediateKey string
sshHostPubKey, sshHostKey string
sshUserPubKey, sshUserKey string
config, defaults string
ottPublicKey *jose.JSONWebKey
ottPrivateKey *jose.JSONWebEncryption
options *options
linkedca.Configuration
Defaults linkedca.Defaults
casOptions apiv1.Options
caService apiv1.CertificateAuthorityService
caCreator apiv1.CertificateAuthorityCreator
// root, rootKey, rootFingerprint string
// intermediate, intermediateKey string
// sshHostPubKey, sshHostKey string
// sshUserPubKey, sshUserKey string
config string
defaults string
// rootFingerprint string
ottPublicKey *jose.JSONWebKey
ottPrivateKey *jose.JSONWebEncryption
options *options
}
// New creates a new PKI configuration.
@ -264,20 +276,6 @@ func New(o apiv1.Options, opts ...PKIOption) (*PKI, error) {
caCreator = creator
}
public := GetPublicPath()
private := GetSecretsPath()
config := GetConfigPath()
// Create directories
dirs := []string{public, private, config, GetTemplatesPath()}
for _, name := range dirs {
if _, err := os.Stat(name); os.IsNotExist(err) {
if err = os.MkdirAll(name, 0700); err != nil {
return nil, errs.FileError(err, name)
}
}
}
// get absolute path for dir/name
getPath := func(dir string, name string) (string, error) {
s, err := filepath.Abs(filepath.Join(dir, name))
@ -285,52 +283,96 @@ func New(o apiv1.Options, opts ...PKIOption) (*PKI, error) {
}
p := &PKI{
Configuration: linkedca.Configuration{
Address: "127.0.0.1:9000",
DnsNames: []string{"127.0.0.1"},
Ssh: &linkedca.SSH{},
Files: make(map[string][]byte),
},
casOptions: o,
caCreator: caCreator,
caService: caService,
options: &options{
provisioner: "step-cli",
address: "127.0.0.1:9000",
dnsNames: []string{"127.0.0.1"},
},
}
for _, fn := range opts {
fn(p.options)
fn(p)
}
if p.root, err = getPath(public, "root_ca.crt"); err != nil {
return nil, err
}
if p.rootKey, err = getPath(private, "root_ca_key"); err != nil {
return nil, err
}
if p.intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
return nil, err
}
if p.intermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil {
return nil, err
}
if p.sshHostPubKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil {
return nil, err
}
if p.sshUserPubKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
return nil, err
}
if p.sshHostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
return nil, err
}
if p.sshUserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
return nil, err
}
if len(config) > 0 {
if p.config, err = getPath(config, "ca.json"); err != nil {
return nil, err
}
if p.defaults, err = getPath(config, "defaults.json"); err != nil {
return nil, err
// Use /home/step as the step path in helm configurations.
// Use the current step path when creating pki in files.
var public, private, config string
if p.options.isHelm {
public = "/home/step/certs"
private = "/home/step/secrets"
config = "/home/step/config"
} else {
public = GetPublicPath()
private = GetSecretsPath()
config = GetConfigPath()
// Create directories
dirs := []string{public, private, config, GetTemplatesPath()}
for _, name := range dirs {
if _, err := os.Stat(name); os.IsNotExist(err) {
if err = os.MkdirAll(name, 0700); err != nil {
return nil, errs.FileError(err, name)
}
}
}
}
if p.Defaults.CaUrl == "" {
p.Defaults.CaUrl = p.DnsNames[0]
_, port, err := net.SplitHostPort(p.Address)
if err != nil {
return nil, errors.Wrapf(err, "error parsing %s", p.Address)
}
if port == "443" {
p.Defaults.CaUrl = fmt.Sprintf("https://%s", p.Defaults.CaUrl)
} else {
p.Defaults.CaUrl = fmt.Sprintf("https://%s:%s", p.Defaults.CaUrl, port)
}
}
root, err := getPath(public, "root_ca.crt")
if err != nil {
return nil, err
}
rootKey, err := getPath(private, "root_ca_key")
if err != nil {
return nil, err
}
p.Root = []string{root}
p.RootKey = []string{rootKey}
p.Defaults.Root = root
if p.Intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
return nil, err
}
if p.IntermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil {
return nil, err
}
if p.Ssh.HostPublicKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil {
return nil, err
}
if p.Ssh.UserPublicKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
return nil, err
}
if p.Ssh.HostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
return nil, err
}
if p.Ssh.UserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
return nil, err
}
if p.defaults, err = getPath(config, "defaults.json"); err != nil {
return nil, err
}
if p.config, err = getPath(config, "ca.json"); err != nil {
return nil, err
}
p.Defaults.CaConfig = p.config
return p, nil
}
@ -341,7 +383,7 @@ func (p *PKI) GetCAConfigPath() string {
// GetRootFingerprint returns the root fingerprint.
func (p *PKI) GetRootFingerprint() string {
return p.rootFingerprint
return p.Defaults.Fingerprint
}
// SetProvisioner sets the provisioner name of the OTT keys.
@ -355,21 +397,21 @@ func (p *PKI) SetProvisioner(s string) {
//
// Deprecated: this method is deprecated in favor of WithAddress.
func (p *PKI) SetAddress(s string) {
p.options.address = s
p.Address = s
}
// SetDNSNames sets the dns names of the CA.
//
// Deprecated: this method is deprecated in favor of WithDNSNames.
func (p *PKI) SetDNSNames(s []string) {
p.options.dnsNames = s
p.DnsNames = s
}
// SetCAURL sets the ca-url to use in the defaults.json.
//
// Deprecated: this method is deprecated in favor of WithCaUrl.
func (p *PKI) SetCAURL(s string) {
p.options.caURL = s
p.Defaults.CaUrl = s
}
// GenerateKeyPairs generates the key pairs used by the certificate authority.
@ -408,11 +450,19 @@ func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (
return nil, err
}
// PrivateKey will only be set if we have access to it (SoftCAS).
if err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil {
sum := sha256.Sum256(resp.Certificate.Raw)
p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
p.Files[p.Root[0]] = encodeCertificate(resp.Certificate)
p.Files[p.RootKey[0]], err = encodePrivateKey(resp.PrivateKey, pass)
if err != nil {
return nil, err
}
// PrivateKey will only be set if we have access to it (SoftCAS).
// if err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil {
// return nil, err
// }
return resp, nil
}
@ -442,12 +492,24 @@ func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent
}
p.casOptions.CertificateAuthority = resp.Name
return p.WriteIntermediateCertificate(resp.Certificate, resp.PrivateKey, pass)
p.Files[p.Intermediate] = encodeCertificate(resp.Certificate)
p.Files[p.IntermediateKey], err = encodePrivateKey(resp.PrivateKey, pass)
if err != nil {
return err
}
return nil
// return p.WriteIntermediateCertificate(resp.Certificate, resp.PrivateKey, pass)
}
// WriteRootCertificate writes to disk the given certificate and key.
func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {
if err := fileutil.WriteFile(p.root, pem.EncodeToMemory(&pem.Block{
fmt.Println(p.options.isHelm)
if p.options.isHelm {
return nil
}
if err := fileutil.WriteFile(p.Root[0], pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: rootCrt.Raw,
}), 0600); err != nil {
@ -455,28 +517,32 @@ func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{
}
if rootKey != nil {
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600))
_, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.RootKey[0], 0600))
if err != nil {
return err
}
}
sum := sha256.Sum256(rootCrt.Raw)
p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
return nil
}
// WriteIntermediateCertificate writes to disk the given certificate and key.
func (p *PKI) WriteIntermediateCertificate(crt *x509.Certificate, key interface{}, pass []byte) error {
if err := fileutil.WriteFile(p.intermediate, pem.EncodeToMemory(&pem.Block{
if p.options.isHelm {
return nil
}
if err := fileutil.WriteFile(p.Intermediate, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
}), 0600); err != nil {
return err
}
if key != nil {
_, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.intermediateKey, 0600))
_, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.IntermediateKey, 0600))
if err != nil {
return err
}
@ -516,8 +582,8 @@ func (p *PKI) GetCertificateAuthority() error {
}
// Issuer is in the RA
p.intermediate = ""
p.intermediateKey = ""
p.Intermediate = ""
p.IntermediateKey = ""
return nil
}
@ -525,8 +591,8 @@ func (p *PKI) GetCertificateAuthority() error {
// GenerateSSHSigningKeys generates and encrypts a private key used for signing
// SSH user certificates and a private key used for signing host certificates.
func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
var pubNames = []string{p.sshHostPubKey, p.sshUserPubKey}
var privNames = []string{p.sshHostKey, p.sshUserKey}
var pubNames = []string{p.Ssh.HostPublicKey, p.Ssh.UserPublicKey}
var privNames = []string{p.Ssh.HostKey, p.Ssh.UserKey}
for i := 0; i < 2; i++ {
pub, priv, err := keyutil.GenerateDefaultKeyPair()
if err != nil {
@ -539,13 +605,19 @@ func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
if err != nil {
return errors.Wrapf(err, "error converting public key")
}
_, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password))
p.Files[pubNames[i]] = ssh.MarshalAuthorizedKey(sshKey)
p.Files[privNames[i]], err = encodePrivateKey(priv, password)
if err != nil {
return err
}
if err = fileutil.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil {
return err
}
// _, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password))
// if err != nil {
// return err
// }
// if err = fileutil.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil {
// return err
// }
}
p.options.enableSSH = true
return nil
@ -575,22 +647,22 @@ func (p *PKI) TellPKI() {
func (p *PKI) tellPKI() {
ui.Println()
if p.casOptions.Is(apiv1.SoftCAS) {
ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root private key", p.rootKey)
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
ui.PrintSelected("Intermediate certificate", p.intermediate)
ui.PrintSelected("Intermediate private key", p.intermediateKey)
} else if p.rootFingerprint != "" {
ui.PrintSelected("Root certificate", p.root)
ui.PrintSelected("Root fingerprint", p.rootFingerprint)
ui.PrintSelected("Root certificate", p.Root[0])
ui.PrintSelected("Root private key", p.RootKey[0])
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
ui.PrintSelected("Intermediate certificate", p.Intermediate)
ui.PrintSelected("Intermediate private key", p.IntermediateKey)
} else if p.Defaults.Fingerprint != "" {
ui.PrintSelected("Root certificate", p.Root[0])
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
} else {
ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad)
}
if p.options.enableSSH {
ui.PrintSelected("SSH user root certificate", p.sshUserPubKey)
ui.PrintSelected("SSH user root private key", p.sshUserKey)
ui.PrintSelected("SSH host root certificate", p.sshHostPubKey)
ui.PrintSelected("SSH host root private key", p.sshHostKey)
ui.PrintSelected("SSH user public key", p.Ssh.UserPublicKey)
ui.PrintSelected("SSH user private key", p.Ssh.UserKey)
ui.PrintSelected("SSH host public key", p.Ssh.HostPublicKey)
ui.PrintSelected("SSH host private key", p.Ssh.HostKey)
}
}
@ -637,12 +709,12 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
}
config := &authconfig.Config{
Root: []string{p.root},
FederatedRoots: []string{},
IntermediateCert: p.intermediate,
IntermediateKey: p.intermediateKey,
Address: p.options.address,
DNSNames: p.options.dnsNames,
Root: p.Root,
FederatedRoots: p.FederatedRoots,
IntermediateCert: p.Intermediate,
IntermediateKey: p.IntermediateKey,
Address: p.Address,
DNSNames: p.DnsNames,
Logger: []byte(`{"format": "text"}`),
DB: &db.Config{
Type: "badger",
@ -685,8 +757,8 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
if p.options.enableSSH {
enableSSHCA := true
config.SSH = &authconfig.SSHConfig{
HostKey: p.sshHostKey,
UserKey: p.sshUserKey,
HostKey: p.Ssh.HostKey,
UserKey: p.Ssh.UserKey,
}
// Enable SSH authorization for default JWK provisioner
prov.Claims = &provisioner.Claims{
@ -776,26 +848,12 @@ func (p *PKI) Save(opt ...Option) error {
return errs.FileError(err, p.config)
}
// Generate the CA URL.
if p.options.caURL == "" {
p.options.caURL = p.options.dnsNames[0]
_, port, err := net.SplitHostPort(p.options.address)
if err != nil {
return errors.Wrapf(err, "error parsing %s", p.options.address)
}
if port == "443" {
p.options.caURL = fmt.Sprintf("https://%s", p.options.caURL)
} else {
p.options.caURL = fmt.Sprintf("https://%s:%s", p.options.caURL, port)
}
}
// Generate and write defaults.json
defaults := &caDefaults{
Root: p.root,
CAConfig: p.config,
CAUrl: p.options.caURL,
Fingerprint: p.rootFingerprint,
Root: p.Defaults.Root,
CAConfig: p.Defaults.CaConfig,
CAUrl: p.Defaults.CaUrl,
Fingerprint: p.Defaults.Fingerprint,
}
b, err = json.MarshalIndent(defaults, "", "\t")
if err != nil {
@ -830,3 +888,26 @@ func (p *PKI) Save(opt ...Option) error {
return nil
}
func encodeCertificate(c *x509.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
})
}
func encodePublicKey(key crypto.PublicKey) ([]byte, error) {
block, err := pemutil.Serialize(key)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(block), nil
}
func encodePrivateKey(key crypto.PrivateKey, pass []byte) ([]byte, error) {
block, err := pemutil.Serialize(key, pemutil.WithPassword(pass))
if err != nil {
return nil, err
}
return pem.EncodeToMemory(block), nil
}