From e71072d389b0002ef397c86dd2f0a1a9a89cd5e6 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 2 Aug 2019 17:48:34 -0700 Subject: [PATCH] Add experimental support for provisioning users. --- api/ssh.go | 42 +++++-- authority/config.go | 6 +- authority/provisioner/sign_ssh_options.go | 15 +++ authority/ssh.go | 131 +++++++++++++++++++--- 4 files changed, 170 insertions(+), 24 deletions(-) diff --git a/api/ssh.go b/api/ssh.go index 7e730bc3..92deff5e 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -14,21 +14,24 @@ import ( // SSHAuthority is the interface implemented by a SSH CA authority. 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) } // SignSSHRequest is the request body of an SSH certificate request. type SignSSHRequest struct { - PublicKey []byte `json:"publicKey"` //base64 encoded - OTT string `json:"ott"` - CertType string `json:"certType"` - Principals []string `json:"principals"` - ValidAfter TimeDuration `json:"validAfter"` - ValidBefore TimeDuration `json:"validBefore"` + PublicKey []byte `json:"publicKey"` //base64 encoded + OTT string `json:"ott"` + CertType string `json:"certType,omitempty"` + Principals []string `json:"principals,omitempty"` + ValidAfter TimeDuration `json:"validAfter,omitempty"` + ValidBefore TimeDuration `json:"validBefore,omitempty"` + AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"` } // SignSSHResponse is the response object that returns the SSH certificate. type SignSSHResponse struct { - Certificate SSHCertificate `json:"crt"` + Certificate SSHCertificate `json:"crt"` + AddUserCertificate SSHCertificate `json:"addUserCrt"` } // SSHCertificate represents the response SSH certificate. @@ -53,6 +56,10 @@ func (c *SSHCertificate) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &s); err != nil { return errors.Wrap(err, "error decoding certificate") } + if s == "" { + c.Certificate = nil + return nil + } certData, err := base64.StdEncoding.DecodeString(s) if err != nil { return errors.Wrap(err, "error decoding certificate") @@ -105,6 +112,15 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { return } + var addUserPublicKey ssh.PublicKey + if body.AddUserPublicKey != nil { + addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey) + if err != nil { + WriteError(w, BadRequest(errors.Wrap(err, "error parsing addUserPublicKey"))) + return + } + } + opts := provisioner.SSHOptions{ CertType: body.CertType, Principals: body.Principals, @@ -125,9 +141,19 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) { return } + var addUserCert *ssh.Certificate + if addUserPublicKey != nil && cert.CertType == ssh.UserCert && len(cert.ValidPrincipals) == 1 { + addUserCert, err = h.Authority.SignSSHAddUser(addUserPublicKey, cert) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + } + w.WriteHeader(http.StatusCreated) // logCertificate(w, cert) JSON(w, &SignSSHResponse{ - Certificate: SSHCertificate{cert}, + Certificate: SSHCertificate{cert}, + AddUserCertificate: SSHCertificate{addUserCert}, }) } diff --git a/authority/config.go b/authority/config.go index 4117d279..412f54db 100644 --- a/authority/config.go +++ b/authority/config.go @@ -101,8 +101,10 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error { // SSHConfig contains the user and host keys. type SSHConfig struct { - HostKey string `json:"hostKey"` - UserKey string `json:"userKey"` + 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 diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index d1bf78bd..83f4ee15 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -14,6 +14,9 @@ const ( // SSHHostCert is the string used to represent ssh.HostCert. SSHHostCert = "host" + + // sshProvisionerCommand is the provisioner command + sshProvisionerCommand = "sudo adduser --quiet --disabled-password --gecos '' %s 2>/dev/null ; nc -q0 localhost 22" ) // SSHCertificateModifier is the interface used to change properties in an SSH @@ -188,6 +191,18 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error { } } +type sshProvisionerExtensionModifier string + +func (m sshProvisionerExtensionModifier) Modify(cert *ssh.Certificate) error { + if cert.CertType == ssh.UserCert { + if cert.CriticalOptions == nil { + cert.CriticalOptions = make(map[string]string) + } + cert.CriticalOptions["force-command"] = fmt.Sprintf(sshProvisionerCommand, m) + } + return nil +} + // sshCertificateValidityModifier is a SSHCertificateModifier checks the // validity bounds, setting them if they are not provided. It will fail if a // CertType has not been set or is not valid. diff --git a/authority/ssh.go b/authority/ssh.go index d6f9dc4c..6dd37a67 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -2,9 +2,7 @@ package authority import ( "crypto/rand" - "crypto/sha256" "encoding/binary" - "encoding/hex" "net/http" "strings" @@ -14,10 +12,17 @@ import ( "golang.org/x/crypto/ssh" ) -func generateSSHPublicKeyID(key ssh.PublicKey) string { - sum := sha256.Sum256(key.Marshal()) - return strings.ToLower(hex.EncodeToString(sum[:])) -} +const ( + // SSHAddUserPrincipal is the principal that will run the add user command. + // Defaults to "provisioner" but it can be changed in the configuration. + SSHAddUserPrincipal = "provisioner" + + // SSHAddUserCommand is the default command to run to add a new user. + // Defaults to "sudo useradd -m ; nc -q0 localhost 22" but it can be changed in the + // configuration. The string "" will be replace by the new + // principal to add. + SSHAddUserCommand = "sudo useradd -m ; nc -q0 localhost 22" +) // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { @@ -38,7 +43,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // validate the given SSHOptions case provisioner.SSHCertificateOptionsValidator: if err := o.Valid(opts); err != nil { - return nil, &apiError{err: err, code: http.StatusUnauthorized} + return nil, &apiError{err: err, code: http.StatusForbidden} } default: return nil, &apiError{ @@ -50,12 +55,15 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign nonce, err := randutil.ASCII(32) if err != nil { - return nil, err + return nil, &apiError{err: err, code: http.StatusInternalServerError} } var serial uint64 if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { - return nil, errors.Wrap(err, "error reading random number") + return nil, &apiError{ + err: errors.Wrap(err, "signSSH: error reading random number"), + code: http.StatusInternalServerError, + } } // Build base certificate with the key and some random values @@ -67,13 +75,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // Use opts to modify the certificate if err := opts.Modify(cert); err != nil { - return nil, err + return nil, &apiError{err: err, code: http.StatusForbidden} } // Use provisioner modifiers for _, m := range mods { if err := m.Modify(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusInternalServerError} + return nil, &apiError{err: err, code: http.StatusForbidden} } } @@ -108,7 +116,7 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign } default: return nil, &apiError{ - err: errors.Errorf("unexpected ssh certificate type: %d", cert.CertType), + err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType), code: http.StatusInternalServerError, } } @@ -121,16 +129,111 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign // Sign the certificate sig, err := signer.Sign(rand.Reader, data) if err != nil { - return nil, err + return nil, &apiError{ + err: errors.Wrap(err, "signSSH: error signing certificate"), + code: http.StatusInternalServerError, + } } cert.Signature = sig // User provisioners validators for _, v := range validators { if err := v.Valid(cert); err != nil { - return nil, &apiError{err: err, code: http.StatusUnauthorized} + return nil, &apiError{err: err, code: http.StatusForbidden} } } return cert, nil } + +// SignSSHAddUser signs a certificate that provisions a new user in a server. +func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) { + if a.sshCAUserCertSignKey == nil { + return nil, &apiError{ + err: errors.New("signSSHProxy: user certificate signing is not enabled"), + code: http.StatusNotImplemented, + } + } + if subject.CertType != ssh.UserCert { + return nil, &apiError{ + err: errors.New("signSSHProxy: certificate is not a user certificate"), + code: http.StatusForbidden, + } + } + if len(subject.ValidPrincipals) != 1 { + return nil, &apiError{ + err: errors.New("signSSHProxy: certificate does not have only one principal"), + code: http.StatusForbidden, + } + } + + nonce, err := randutil.ASCII(32) + if err != nil { + return nil, &apiError{err: err, code: http.StatusInternalServerError} + } + + var serial uint64 + if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "signSSHProxy: error reading random number"), + code: http.StatusInternalServerError, + } + } + + signer, err := ssh.NewSignerFromSigner(a.sshCAUserCertSignKey) + if err != nil { + return nil, &apiError{ + err: errors.Wrap(err, "signSSHProxy: error creating signer"), + code: http.StatusInternalServerError, + } + } + + principal := subject.ValidPrincipals[0] + addUserPrincipal := a.getAddUserPrincipal() + + cert := &ssh.Certificate{ + Nonce: []byte(nonce), + Key: key, + Serial: serial, + CertType: ssh.UserCert, + KeyId: principal + "-" + addUserPrincipal, + ValidPrincipals: []string{addUserPrincipal}, + ValidAfter: subject.ValidAfter, + ValidBefore: subject.ValidBefore, + Permissions: ssh.Permissions{ + CriticalOptions: map[string]string{ + "force-command": a.getAddUserCommand(principal), + }, + }, + SignatureKey: signer.PublicKey(), + } + + // Get bytes for signing trailing the signature length. + data := cert.Marshal() + data = data[:len(data)-4] + + // Sign the certificate + sig, err := signer.Sign(rand.Reader, data) + if err != nil { + return nil, err + } + cert.Signature = sig + return cert, nil +} + +func (a *Authority) getAddUserPrincipal() (cmd string) { + if a.config.SSH.AddUserPrincipal == "" { + return SSHAddUserPrincipal + } + return a.config.SSH.AddUserPrincipal +} + +func (a *Authority) getAddUserCommand(principal string) string { + var cmd string + if a.config.SSH.AddUserCommand == "" { + cmd = SSHAddUserCommand + } else { + cmd = a.config.SSH.AddUserCommand + } + return strings.Replace(cmd, "", principal, -1) +}