Skip to content

Commit fbdc289

Browse files
committed
Add ability to inject critical options into certs
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.
1 parent f130a8d commit fbdc289

File tree

4 files changed

+75
-0
lines changed

4 files changed

+75
-0
lines changed

README.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,15 @@ Effectively the format is::
315315
indicate which users are allowed to sign requests.
316316
- ``AuthorizedUsers``: Same as ``AuthorizedSigners`` except that these are
317317
fingerprints of people allowed to submit requests.
318+
- ``CriticalOptions``: A hash of critical options to be added to all
319+
certificate requests. By specifying these in your configuration file
320+
all cert requests to this environment will have these options embedded
321+
in them. You can use this option, for example, to restrict the IP
322+
addresses that are allowed to use a certificate or to force a user
323+
to only be able to run a single command. Those are the only two
324+
options supported by sshd right now. This document describes them in
325+
the section ``Critical options``:
326+
http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
318327

319328
The same users and fingerprints may appear in both ``AuthorizedSigners`` and
320329
``AuthorizedUsers``.

sign_certd.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ import (
3030
"time"
3131
)
3232

33+
// Yanked from PROTOCOL.certkeys
34+
var supportedCriticalOptions = []string{
35+
"force-command",
36+
"source-address",
37+
}
38+
39+
func isSupportedOption(x string) bool {
40+
for optionIdx := range supportedCriticalOptions {
41+
if supportedCriticalOptions[optionIdx] == x {
42+
return true
43+
}
44+
}
45+
return false
46+
}
47+
48+
func areCriticalOptionsValid(criticalOptions map[string]string) error {
49+
for optionName, _ := range criticalOptions {
50+
if !isSupportedOption(optionName) {
51+
return fmt.Errorf("Invalid critical option name: '%s'", optionName)
52+
}
53+
}
54+
return nil
55+
}
56+
3357
type certRequest struct {
3458
// This struct tracks state for certificate requests. Imagine this one day
3559
// being stored in a persistent data store.
@@ -192,13 +216,24 @@ func (h *certRequestHandler) createSigningRequest(rw http.ResponseWriter, req *h
192216
http.Error(rw, "Unknown environment.", http.StatusBadRequest)
193217
return
194218
}
219+
195220
err = h.validateCert(cert, config.AuthorizedUsers)
196221
if err != nil {
197222
log.Printf("Invalid certificate signing request received from %s, ignoring", req.RemoteAddr)
198223
http.Error(rw, fmt.Sprintf("%v", err), http.StatusBadRequest)
199224
return
200225
}
201226

227+
// Ideally we put the critical options into the cert and let validateCert
228+
// do the validation. However, this also checks the signature on the cert
229+
// which would fail if we modified it prior to validation. So we validate
230+
// by hand.
231+
if len(config.CriticalOptions) > 0 {
232+
for optionName, optionVal := range config.CriticalOptions {
233+
cert.CriticalOptions[optionName] = optionVal
234+
}
235+
}
236+
202237
requestID := make([]byte, 10)
203238
rand.Reader.Read(requestID)
204239
requestIDStr := base32.StdEncoding.EncodeToString(requestID)
@@ -324,6 +359,8 @@ func (h *certRequestHandler) validateCert(cert *ssh.Certificate, authorizedSigne
324359
_, ok := authorizedSigners[fingerprint]
325360
return ok
326361
}
362+
certChecker.SupportedCriticalOptions = supportedCriticalOptions
363+
327364
err := certChecker.CheckCert(cert.ValidPrincipals[0], cert)
328365
if err != nil {
329366
err := fmt.Errorf("Cert not valid: %v", err)
@@ -584,6 +621,12 @@ func signCertd(c *cli.Context) error {
584621
if err != nil {
585622
return cli.NewExitError(fmt.Sprintf("Load Config failed: %s", err), 1)
586623
}
624+
for envName, configObj := range config {
625+
err = areCriticalOptionsValid(configObj.CriticalOptions)
626+
if err != nil {
627+
return cli.NewExitError(fmt.Sprintf("Error validation config for env '%s': %s", envName, err), 1)
628+
}
629+
}
587630
err = runSignCertd(config)
588631
return err
589632
}

sign_certd_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,28 @@ func TestSaveRequestInvalidCert(t *testing.T) {
255255
}
256256
}
257257

258+
func TestSaveRequestInvalidCriticalOptions(t *testing.T) {
259+
allConfig := SetupSignerdConfig(1, 0)
260+
environment := "testing"
261+
envConfig := allConfig[environment]
262+
envConfig.CriticalOptions = make(map[string]string)
263+
envConfig.CriticalOptions["non-existent-critical"] = "yes"
264+
if areCriticalOptionsValid(envConfig.CriticalOptions) == nil {
265+
t.Fatalf("Should have found invalid critical option and didn't")
266+
}
267+
}
268+
269+
func TestSaveRequestValidCriticalOptions(t *testing.T) {
270+
allConfig := SetupSignerdConfig(1, 0)
271+
environment := "testing"
272+
envConfig := allConfig[environment]
273+
envConfig.CriticalOptions = make(map[string]string)
274+
envConfig.CriticalOptions["force-command"] = "/bin/ls"
275+
if areCriticalOptionsValid(envConfig.CriticalOptions) != nil {
276+
t.Fatalf("Critical option is valid. But our test failed.")
277+
}
278+
}
279+
258280
func getTwoBoringCerts(t *testing.T) (*ssh.Certificate, *ssh.Certificate) {
259281
pubKeyOne, _, _, _, err := ssh.ParseAuthorizedKey([]byte(boringUserCertString))
260282
if err != nil {

util/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type SignerdConfig struct {
2222
MaxCertLifetime int
2323
PrivateKeyFile string
2424
KmsRegion string
25+
CriticalOptions map[string]string
2526
}
2627

2728
type SignerConfig struct {

0 commit comments

Comments
 (0)