Add support for multiple ssh roots.

Fixes #125
This commit is contained in:
Mariano Cano 2019-10-08 18:09:41 -07:00 committed by max furman
parent 91130b9c3f
commit b5bc249e1c
6 changed files with 174 additions and 46 deletions

View file

@ -253,6 +253,7 @@ func (h *caHandler) Route(r Router) {
// SSH CA // SSH CA
r.MethodFunc("POST", "/ssh/sign", h.SignSSH) r.MethodFunc("POST", "/ssh/sign", h.SignSSH)
r.MethodFunc("GET", "/ssh/keys", h.SSHKeys) r.MethodFunc("GET", "/ssh/keys", h.SSHKeys)
r.MethodFunc("GET", "/ssh/federation", h.SSHFederatedKeys)
r.MethodFunc("POST", "/ssh/config", h.SSHConfig) r.MethodFunc("POST", "/ssh/config", h.SSHConfig)
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig) r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)

View file

@ -18,6 +18,7 @@ type SSHAuthority interface {
SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
GetSSHKeys() (*authority.SSHKeys, error) GetSSHKeys() (*authority.SSHKeys, error)
GetSSHFederatedKeys() (*authority.SSHKeys, error)
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
} }
@ -55,8 +56,8 @@ type SignSSHResponse struct {
// SSHKeysResponse represents the response object that returns the SSH user and // SSHKeysResponse represents the response object that returns the SSH user and
// host keys. // host keys.
type SSHKeysResponse struct { type SSHKeysResponse struct {
UserKey *SSHPublicKey `json:"userKey,omitempty"` UserKeys []SSHPublicKey `json:"userKey,omitempty"`
HostKey *SSHPublicKey `json:"hostKey,omitempty"` HostKeys []SSHPublicKey `json:"hostKey,omitempty"`
} }
// SSHCertificate represents the response SSH certificate. // SSHCertificate represents the response SSH certificate.
@ -241,23 +242,50 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) {
// certificates. // certificates.
func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) { func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) {
keys, err := h.Authority.GetSSHKeys() keys, err := h.Authority.GetSSHKeys()
if err != nil {
WriteError(w, InternalServerError(err))
return
}
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
WriteError(w, NotFound(errors.New("no keys found")))
return
}
resp := new(SSHKeysResponse)
for _, k := range keys.HostKeys {
resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})
}
for _, k := range keys.UserKeys {
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
}
JSON(w, resp)
}
// SSHFederatedKeys is an HTTP handler that returns the federated SSH public
// keys for user and host certificates.
func (h *caHandler) SSHFederatedKeys(w http.ResponseWriter, r *http.Request) {
keys, err := h.Authority.GetSSHFederatedKeys()
if err != nil { if err != nil {
WriteError(w, NotFound(err)) WriteError(w, NotFound(err))
return return
} }
var host, user *SSHPublicKey if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
if keys.HostKey != nil { WriteError(w, NotFound(errors.New("no keys found")))
host = &SSHPublicKey{PublicKey: keys.HostKey} return
}
if keys.UserKey != nil {
user = &SSHPublicKey{PublicKey: keys.UserKey}
} }
JSON(w, &SSHKeysResponse{ resp := new(SSHKeysResponse)
HostKey: host, for _, k := range keys.HostKeys {
UserKey: user, resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})
}) }
for _, k := range keys.UserKeys {
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
}
JSON(w, resp)
} }
// SSHConfig is an HTTP handler that returns rendered templates for ssh clients // SSHConfig is an HTTP handler that returns rendered templates for ssh clients

View file

@ -24,15 +24,19 @@ const (
// Authority implements the Certificate Authority internal interface. // Authority implements the Certificate Authority internal interface.
type Authority struct { type Authority struct {
config *Config config *Config
rootX509Certs []*x509.Certificate rootX509Certs []*x509.Certificate
intermediateIdentity *x509util.Identity intermediateIdentity *x509util.Identity
sshCAUserCertSignKey ssh.Signer sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer
certificates *sync.Map sshCAUserCerts []ssh.PublicKey
startTime time.Time sshCAHostCerts []ssh.PublicKey
provisioners *provisioner.Collection sshCAUserFederatedCerts []ssh.PublicKey
db db.AuthDB sshCAHostFederatedCerts []ssh.PublicKey
certificates *sync.Map
startTime time.Time
provisioners *provisioner.Collection
db db.AuthDB
// Do not re-initialize // Do not re-initialize
initOnce bool initOnce bool
} }
@ -136,6 +140,9 @@ func (a *Authority) init() error {
if err != nil { if err != nil {
return errors.Wrap(err, "error creating ssh signer") return errors.Wrap(err, "error creating ssh signer")
} }
// Append public key to list of host certs
a.sshCAHostCerts = append(a.sshCAHostCerts, a.sshCAHostCertSignKey.PublicKey())
a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey())
} }
if a.config.SSH.UserKey != "" { if a.config.SSH.UserKey != "" {
signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password) signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password)
@ -146,6 +153,29 @@ func (a *Authority) init() error {
if err != nil { if err != nil {
return errors.Wrap(err, "error creating ssh signer") return errors.Wrap(err, "error creating ssh signer")
} }
// Append public key to list of user certs
a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAHostCertSignKey.PublicKey())
a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey())
}
// Append other public keys
for _, key := range a.config.SSH.Keys {
switch key.Type {
case provisioner.SSHHostCert:
if key.Federated {
a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, key.PublicKey())
} else {
a.sshCAHostCerts = append(a.sshCAHostCerts, key.PublicKey())
}
case provisioner.SSHUserCert:
if key.Federated {
a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, key.PublicKey())
} else {
a.sshCAUserCerts = append(a.sshCAUserCerts, key.PublicKey())
}
default:
return errors.Errorf("unsupported type %s", key.Type)
}
} }
} }

View file

@ -104,14 +104,6 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
return nil return nil
} }
// SSHConfig contains the user and host keys.
type SSHConfig struct {
HostKey string `json:"hostKey"`
UserKey string `json:"userKey"`
AddUserPrincipal string `json:"addUserPrincipal"`
AddUserCommand string `json:"addUserCommand"`
}
// LoadConfiguration parses the given filename in JSON format and returns the // LoadConfiguration parses the given filename in JSON format and returns the
// configuration struct. // configuration struct.
func LoadConfiguration(filename string) (*Config, error) { func LoadConfiguration(filename string) (*Config, error) {
@ -184,6 +176,11 @@ func (c *Config) Validate() error {
c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation c.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation
} }
// Validate ssh: nil is ok
if err := c.SSH.Validate(); err != nil {
return err
}
// Validate templates: nil is ok // Validate templates: nil is ok
if err := c.Templates.Validate(); err != nil { if err := c.Templates.Validate(); err != nil {
return err return err

View file

@ -10,6 +10,7 @@ import (
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/templates" "github.com/smallstep/certificates/templates"
"github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
@ -25,28 +26,81 @@ const (
SSHAddUserCommand = "sudo useradd -m <principal>; nc -q0 localhost 22" SSHAddUserCommand = "sudo useradd -m <principal>; nc -q0 localhost 22"
) )
// SSHConfig contains the user and host keys.
type SSHConfig struct {
HostKey string `json:"hostKey"`
UserKey string `json:"userKey"`
Keys []*SSHPublicKey `json:"keys,omitempty"`
AddUserPrincipal string `json:"addUserPrincipal"`
AddUserCommand string `json:"addUserCommand"`
}
// Validate checks the fields in SSHConfig.
func (c *SSHConfig) Validate() error {
if c == nil {
return nil
}
for _, k := range c.Keys {
if err := k.Validate(); err != nil {
return err
}
}
return nil
}
// SSHPublicKey contains a public key used by federated CAs to keep old signing
// keys for this ca.
type SSHPublicKey struct {
Type string `json:"type"`
Federated bool `json:"federated"`
Key jose.JSONWebKey `json:"key"`
publicKey ssh.PublicKey
}
// Validate checks the fields in SSHPublicKey.
func (k *SSHPublicKey) Validate() error {
switch {
case k.Type == "":
return errors.New("type cannot be empty")
case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:
return errors.Errorf("invalid type %s, it must be user or host", k.Type)
case !k.Key.IsPublic():
return errors.New("invalid key type, it must be a public key")
}
key, err := ssh.NewPublicKey(k.Key.Key)
if err != nil {
return errors.Wrap(err, "error creating ssh key")
}
k.publicKey = key
return nil
}
// PublicKey returns the ssh public key.
func (k *SSHPublicKey) PublicKey() ssh.PublicKey {
return k.publicKey
}
// SSHKeys represents the SSH User and Host public keys. // SSHKeys represents the SSH User and Host public keys.
type SSHKeys struct { type SSHKeys struct {
UserKey ssh.PublicKey UserKeys []ssh.PublicKey
HostKey ssh.PublicKey HostKeys []ssh.PublicKey
} }
// GetSSHKeys returns the SSH User and Host public keys. // GetSSHKeys returns the SSH User and Host public keys.
func (a *Authority) GetSSHKeys() (*SSHKeys, error) { func (a *Authority) GetSSHKeys() (*SSHKeys, error) {
var keys SSHKeys return &SSHKeys{
if a.sshCAUserCertSignKey != nil { HostKeys: a.sshCAHostCerts,
keys.UserKey = a.sshCAUserCertSignKey.PublicKey() UserKeys: a.sshCAUserCerts,
} }, nil
if a.sshCAHostCertSignKey != nil { }
keys.HostKey = a.sshCAHostCertSignKey.PublicKey()
} // GetSSHFederatedKeys returns the public keys for federated SSH signers.
if keys.UserKey == nil && keys.HostKey == nil { func (a *Authority) GetSSHFederatedKeys() (*SSHKeys, error) {
return nil, &apiError{ return &SSHKeys{
err: errors.New("getSSHKeys: ssh is not configured"), HostKeys: a.sshCAHostFederatedCerts,
code: http.StatusNotFound, UserKeys: a.sshCAUserFederatedCerts,
} }, nil
}
return &keys, nil
} }
// GetSSHConfig returns rendered templates for clients (user) or servers (host). // GetSSHConfig returns rendered templates for clients (user) or servers (host).

View file

@ -527,7 +527,7 @@ func (c *Client) Federation() (*api.FederationResponse, error) {
return &federation, nil return &federation, nil
} }
// SSHKeys performs the get ssh keys request to the CA and returns the // SSHKeys performs the get /ssh/keys request to the CA and returns the
// api.SSHKeysResponse struct. // api.SSHKeysResponse struct.
func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) { func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) {
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"}) u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"})
@ -545,6 +545,24 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) {
return &keys, nil return &keys, nil
} }
// SSHFederation performs the get /ssh/federation request to the CA and returns
// the api.SSHKeysResponse struct.
func (c *Client) SSHFederation() (*api.SSHKeysResponse, error) {
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"})
resp, err := c.client.Get(u.String())
if err != nil {
return nil, errors.Wrapf(err, "client GET %s failed", u)
}
if resp.StatusCode >= 400 {
return nil, readError(resp.Body)
}
var keys api.SSHKeysResponse
if err := readJSON(resp.Body, &keys); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
}
return &keys, nil
}
// SSHConfig performs the POST request to the CA to get the ssh configuration // SSHConfig performs the POST request to the CA to get the ssh configuration
// templates. // templates.
func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) { func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {