forked from TrueCloudLab/certificates
Add experimental support for provisioning users.
This commit is contained in:
parent
390aecca0b
commit
e71072d389
4 changed files with 170 additions and 24 deletions
42
api/ssh.go
42
api/ssh.go
|
@ -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},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
131
authority/ssh.go
131
authority/ssh.go
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue