diff --git a/README.rst b/README.rst index 42c1623..589ba1e 100644 --- a/README.rst +++ b/README.rst @@ -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``. diff --git a/sign_certd.go b/sign_certd.go index 62040f0..c55e61c 100644 --- a/sign_certd.go +++ b/sign_certd.go @@ -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. @@ -192,6 +216,7 @@ 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) @@ -199,6 +224,16 @@ func (h *certRequestHandler) createSigningRequest(rw http.ResponseWriter, req *h 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) @@ -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) @@ -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 } diff --git a/sign_certd_test.go b/sign_certd_test.go index fe01f55..08704af 100644 --- a/sign_certd_test.go +++ b/sign_certd_test.go @@ -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 { diff --git a/util/config.go b/util/config.go index 4c63f80..0a8610d 100644 --- a/util/config.go +++ b/util/config.go @@ -22,6 +22,7 @@ type SignerdConfig struct { MaxCertLifetime int PrivateKeyFile string KmsRegion string + CriticalOptions map[string]string } type SignerConfig struct {