parent
50c8b10a4f
commit
9f13a92b9e
6 changed files with 174 additions and 46 deletions
|
@ -252,6 +252,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)
|
||||
|
||||
|
|
52
api/ssh.go
52
api/ssh.go
|
@ -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
|
||||
|
|
|
@ -29,6 +29,10 @@ type Authority struct {
|
|||
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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
20
ca/client.go
20
ca/client.go
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue