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.
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},
})
}

View file

@ -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

View file

@ -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.

View file

@ -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 <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.
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>", principal, -1)
}