Skip to content

Commit

Permalink
Add ability to inject critical options into certs
Browse files Browse the repository at this point in the history
You may now specify CriticalOptions in sign_certd's config on a
per-environment basis. This allows you to write a policy that says all
certs against this environment will have exactly these critical options.
You can ensure that certs always launch users into restricted shells or
from a defined range of source IPs as supported by sshd.
  • Loading branch information
bobveznat committed Sep 19, 2016
1 parent f130a8d commit fbdc289
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 0 deletions.
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@ Effectively the format is::
indicate which users are allowed to sign requests.
- ``AuthorizedUsers``: Same as ``AuthorizedSigners`` except that these are
fingerprints of people allowed to submit requests.
- ``CriticalOptions``: A hash of critical options to be added to all
certificate requests. By specifying these in your configuration file
all cert requests to this environment will have these options embedded
in them. You can use this option, for example, to restrict the IP
addresses that are allowed to use a certificate or to force a user
to only be able to run a single command. Those are the only two
options supported by sshd right now. This document describes them in
the section ``Critical options``:
http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD

The same users and fingerprints may appear in both ``AuthorizedSigners`` and
``AuthorizedUsers``.
Expand Down
43 changes: 43 additions & 0 deletions sign_certd.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ import (
"time"
)

// Yanked from PROTOCOL.certkeys
var supportedCriticalOptions = []string{
"force-command",
"source-address",
}

func isSupportedOption(x string) bool {
for optionIdx := range supportedCriticalOptions {
if supportedCriticalOptions[optionIdx] == x {
return true
}
}
return false
}

func areCriticalOptionsValid(criticalOptions map[string]string) error {
for optionName, _ := range criticalOptions {
if !isSupportedOption(optionName) {
return fmt.Errorf("Invalid critical option name: '%s'", optionName)
}
}
return nil
}

type certRequest struct {
// This struct tracks state for certificate requests. Imagine this one day
// being stored in a persistent data store.
Expand Down Expand Up @@ -192,13 +216,24 @@ func (h *certRequestHandler) createSigningRequest(rw http.ResponseWriter, req *h
http.Error(rw, "Unknown environment.", http.StatusBadRequest)
return
}

err = h.validateCert(cert, config.AuthorizedUsers)
if err != nil {
log.Printf("Invalid certificate signing request received from %s, ignoring", req.RemoteAddr)
http.Error(rw, fmt.Sprintf("%v", err), http.StatusBadRequest)
return
}

// Ideally we put the critical options into the cert and let validateCert
// do the validation. However, this also checks the signature on the cert
// which would fail if we modified it prior to validation. So we validate
// by hand.
if len(config.CriticalOptions) > 0 {
for optionName, optionVal := range config.CriticalOptions {
cert.CriticalOptions[optionName] = optionVal
}
}

requestID := make([]byte, 10)
rand.Reader.Read(requestID)
requestIDStr := base32.StdEncoding.EncodeToString(requestID)
Expand Down Expand Up @@ -324,6 +359,8 @@ func (h *certRequestHandler) validateCert(cert *ssh.Certificate, authorizedSigne
_, ok := authorizedSigners[fingerprint]
return ok
}
certChecker.SupportedCriticalOptions = supportedCriticalOptions

err := certChecker.CheckCert(cert.ValidPrincipals[0], cert)
if err != nil {
err := fmt.Errorf("Cert not valid: %v", err)
Expand Down Expand Up @@ -584,6 +621,12 @@ func signCertd(c *cli.Context) error {
if err != nil {
return cli.NewExitError(fmt.Sprintf("Load Config failed: %s", err), 1)
}
for envName, configObj := range config {
err = areCriticalOptionsValid(configObj.CriticalOptions)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Error validation config for env '%s': %s", envName, err), 1)
}
}
err = runSignCertd(config)
return err
}
Expand Down
22 changes: 22 additions & 0 deletions sign_certd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,28 @@ func TestSaveRequestInvalidCert(t *testing.T) {
}
}

func TestSaveRequestInvalidCriticalOptions(t *testing.T) {
allConfig := SetupSignerdConfig(1, 0)
environment := "testing"
envConfig := allConfig[environment]
envConfig.CriticalOptions = make(map[string]string)
envConfig.CriticalOptions["non-existent-critical"] = "yes"
if areCriticalOptionsValid(envConfig.CriticalOptions) == nil {
t.Fatalf("Should have found invalid critical option and didn't")
}
}

func TestSaveRequestValidCriticalOptions(t *testing.T) {
allConfig := SetupSignerdConfig(1, 0)
environment := "testing"
envConfig := allConfig[environment]
envConfig.CriticalOptions = make(map[string]string)
envConfig.CriticalOptions["force-command"] = "/bin/ls"
if areCriticalOptionsValid(envConfig.CriticalOptions) != nil {
t.Fatalf("Critical option is valid. But our test failed.")
}
}

func getTwoBoringCerts(t *testing.T) (*ssh.Certificate, *ssh.Certificate) {
pubKeyOne, _, _, _, err := ssh.ParseAuthorizedKey([]byte(boringUserCertString))
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type SignerdConfig struct {
MaxCertLifetime int
PrivateKeyFile string
KmsRegion string
CriticalOptions map[string]string
}

type SignerConfig struct {
Expand Down

0 comments on commit fbdc289

Please sign in to comment.