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 caa2174efc
commit e84489775b
6 changed files with 174 additions and 46 deletions

View file

@ -253,6 +253,7 @@ func (h *caHandler) Route(r Router) {
// SSH CA
r.MethodFunc("POST", "/ssh/sign", h.SignSSH)
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/{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)
SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
GetSSHKeys() (*authority.SSHKeys, error)
GetSSHFederatedKeys() (*authority.SSHKeys, 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
// host keys.
type SSHKeysResponse struct {
UserKey *SSHPublicKey `json:"userKey,omitempty"`
HostKey *SSHPublicKey `json:"hostKey,omitempty"`
UserKeys []SSHPublicKey `json:"userKey,omitempty"`
HostKeys []SSHPublicKey `json:"hostKey,omitempty"`
}
// SSHCertificate represents the response SSH certificate.
@ -241,23 +242,50 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) {
// certificates.
func (h *caHandler) SSHKeys(w http.ResponseWriter, r *http.Request) {
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 {
WriteError(w, NotFound(err))
return
}
var host, user *SSHPublicKey
if keys.HostKey != nil {
host = &SSHPublicKey{PublicKey: keys.HostKey}
}
if keys.UserKey != nil {
user = &SSHPublicKey{PublicKey: keys.UserKey}
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
WriteError(w, NotFound(errors.New("no keys found")))
return
}
JSON(w, &SSHKeysResponse{
HostKey: host,
UserKey: user,
})
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)
}
// 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.
type Authority struct {
config *Config
rootX509Certs []*x509.Certificate
intermediateIdentity *x509util.Identity
sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer
certificates *sync.Map
startTime time.Time
provisioners *provisioner.Collection
db db.AuthDB
config *Config
rootX509Certs []*x509.Certificate
intermediateIdentity *x509util.Identity
sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer
sshCAUserCerts []ssh.PublicKey
sshCAHostCerts []ssh.PublicKey
sshCAUserFederatedCerts []ssh.PublicKey
sshCAHostFederatedCerts []ssh.PublicKey
certificates *sync.Map
startTime time.Time
provisioners *provisioner.Collection
db db.AuthDB
// Do not re-initialize
initOnce bool
}
@ -136,6 +140,9 @@ func (a *Authority) init() error {
if err != nil {
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 != "" {
signer, err := parseCryptoSigner(a.config.SSH.UserKey, a.config.Password)
@ -146,6 +153,29 @@ func (a *Authority) init() error {
if err != nil {
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
}
// 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
// configuration struct.
func LoadConfiguration(filename string) (*Config, error) {
@ -184,6 +176,11 @@ func (c *Config) Validate() error {
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
if err := c.Templates.Validate(); err != nil {
return err

View file

@ -10,6 +10,7 @@ import (
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/templates"
"github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
@ -25,28 +26,81 @@ const (
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.
type SSHKeys struct {
UserKey ssh.PublicKey
HostKey ssh.PublicKey
UserKeys []ssh.PublicKey
HostKeys []ssh.PublicKey
}
// GetSSHKeys returns the SSH User and Host public keys.
func (a *Authority) GetSSHKeys() (*SSHKeys, error) {
var keys SSHKeys
if a.sshCAUserCertSignKey != nil {
keys.UserKey = a.sshCAUserCertSignKey.PublicKey()
}
if a.sshCAHostCertSignKey != nil {
keys.HostKey = a.sshCAHostCertSignKey.PublicKey()
}
if keys.UserKey == nil && keys.HostKey == nil {
return nil, &apiError{
err: errors.New("getSSHKeys: ssh is not configured"),
code: http.StatusNotFound,
}
}
return &keys, nil
return &SSHKeys{
HostKeys: a.sshCAHostCerts,
UserKeys: a.sshCAUserCerts,
}, nil
}
// GetSSHFederatedKeys returns the public keys for federated SSH signers.
func (a *Authority) GetSSHFederatedKeys() (*SSHKeys, error) {
return &SSHKeys{
HostKeys: a.sshCAHostFederatedCerts,
UserKeys: a.sshCAUserFederatedCerts,
}, nil
}
// 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
}
// 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.
func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) {
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/keys"})
@ -545,6 +545,24 @@ func (c *Client) SSHKeys() (*api.SSHKeysResponse, error) {
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
// templates.
func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {