parent
91130b9c3f
commit
b5bc249e1c
6 changed files with 174 additions and 46 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
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)
|
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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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).
|
||||||
|
|
20
ca/client.go
20
ca/client.go
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue