Add experimental support for provisioning users.

This commit is contained in:
Mariano Cano 2019-08-02 17:48:34 -07:00
parent 390aecca0b
commit e71072d389
4 changed files with 170 additions and 24 deletions

View file

@ -14,21 +14,24 @@ import (
// SSHAuthority is the interface implemented by a SSH CA authority. // SSHAuthority is the interface implemented by a SSH CA authority.
type SSHAuthority interface { 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)
} }
// SignSSHRequest is the request body of an SSH certificate request. // SignSSHRequest is the request body of an SSH certificate request.
type SignSSHRequest struct { type SignSSHRequest struct {
PublicKey []byte `json:"publicKey"` //base64 encoded PublicKey []byte `json:"publicKey"` //base64 encoded
OTT string `json:"ott"` OTT string `json:"ott"`
CertType string `json:"certType"` CertType string `json:"certType,omitempty"`
Principals []string `json:"principals"` Principals []string `json:"principals,omitempty"`
ValidAfter TimeDuration `json:"validAfter"` ValidAfter TimeDuration `json:"validAfter,omitempty"`
ValidBefore TimeDuration `json:"validBefore"` ValidBefore TimeDuration `json:"validBefore,omitempty"`
AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"`
} }
// SignSSHResponse is the response object that returns the SSH certificate. // SignSSHResponse is the response object that returns the SSH certificate.
type SignSSHResponse struct { type SignSSHResponse struct {
Certificate SSHCertificate `json:"crt"` Certificate SSHCertificate `json:"crt"`
AddUserCertificate SSHCertificate `json:"addUserCrt"`
} }
// SSHCertificate represents the response SSH certificate. // 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 { if err := json.Unmarshal(data, &s); err != nil {
return errors.Wrap(err, "error decoding certificate") return errors.Wrap(err, "error decoding certificate")
} }
if s == "" {
c.Certificate = nil
return nil
}
certData, err := base64.StdEncoding.DecodeString(s) certData, err := base64.StdEncoding.DecodeString(s)
if err != nil { if err != nil {
return errors.Wrap(err, "error decoding certificate") return errors.Wrap(err, "error decoding certificate")
@ -105,6 +112,15 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) {
return 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{ opts := provisioner.SSHOptions{
CertType: body.CertType, CertType: body.CertType,
Principals: body.Principals, Principals: body.Principals,
@ -125,9 +141,19 @@ func (h *caHandler) SignSSH(w http.ResponseWriter, r *http.Request) {
return 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) w.WriteHeader(http.StatusCreated)
// logCertificate(w, cert) // logCertificate(w, cert)
JSON(w, &SignSSHResponse{ JSON(w, &SignSSHResponse{
Certificate: SSHCertificate{cert}, Certificate: SSHCertificate{cert},
AddUserCertificate: SSHCertificate{addUserCert},
}) })
} }

View file

@ -101,8 +101,10 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
// SSHConfig contains the user and host keys. // SSHConfig contains the user and host keys.
type SSHConfig struct { type SSHConfig struct {
HostKey string `json:"hostKey"` HostKey string `json:"hostKey"`
UserKey string `json:"userKey"` 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

View file

@ -14,6 +14,9 @@ const (
// SSHHostCert is the string used to represent ssh.HostCert. // SSHHostCert is the string used to represent ssh.HostCert.
SSHHostCert = "host" 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 // 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 // sshCertificateValidityModifier is a SSHCertificateModifier checks the
// validity bounds, setting them if they are not provided. It will fail if a // validity bounds, setting them if they are not provided. It will fail if a
// CertType has not been set or is not valid. // CertType has not been set or is not valid.

View file

@ -2,9 +2,7 @@ package authority
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/binary" "encoding/binary"
"encoding/hex"
"net/http" "net/http"
"strings" "strings"
@ -14,10 +12,17 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
func generateSSHPublicKeyID(key ssh.PublicKey) string { const (
sum := sha256.Sum256(key.Marshal()) // SSHAddUserPrincipal is the principal that will run the add user command.
return strings.ToLower(hex.EncodeToString(sum[:])) // 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 <principal>; nc -q0 localhost 22" but it can be changed in the
// configuration. The string "<principal>" will be replace by the new
// principal to add.
SSHAddUserCommand = "sudo useradd -m <principal>; nc -q0 localhost 22"
)
// SignSSH creates a signed SSH certificate with the given public key and options. // 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) { 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 // validate the given SSHOptions
case provisioner.SSHCertificateOptionsValidator: case provisioner.SSHCertificateOptionsValidator:
if err := o.Valid(opts); err != nil { if err := o.Valid(opts); err != nil {
return nil, &apiError{err: err, code: http.StatusUnauthorized} return nil, &apiError{err: err, code: http.StatusForbidden}
} }
default: default:
return nil, &apiError{ return nil, &apiError{
@ -50,12 +55,15 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
nonce, err := randutil.ASCII(32) nonce, err := randutil.ASCII(32)
if err != nil { if err != nil {
return nil, err return nil, &apiError{err: err, code: http.StatusInternalServerError}
} }
var serial uint64 var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil { 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 // 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 // Use opts to modify the certificate
if err := opts.Modify(cert); err != nil { if err := opts.Modify(cert); err != nil {
return nil, err return nil, &apiError{err: err, code: http.StatusForbidden}
} }
// Use provisioner modifiers // Use provisioner modifiers
for _, m := range mods { for _, m := range mods {
if err := m.Modify(cert); err != nil { 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: default:
return nil, &apiError{ 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, code: http.StatusInternalServerError,
} }
} }
@ -121,16 +129,111 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
// Sign the certificate // Sign the certificate
sig, err := signer.Sign(rand.Reader, data) sig, err := signer.Sign(rand.Reader, data)
if err != nil { if err != nil {
return nil, err return nil, &apiError{
err: errors.Wrap(err, "signSSH: error signing certificate"),
code: http.StatusInternalServerError,
}
} }
cert.Signature = sig cert.Signature = sig
// User provisioners validators // User provisioners validators
for _, v := range validators { for _, v := range validators {
if err := v.Valid(cert); err != nil { 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 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>", principal, -1)
}