From 5d7e2397226cd4c88a18658d8fc89ca0da58cc49 Mon Sep 17 00:00:00 2001 From: Niall Sheridan Date: Tue, 10 Jan 2017 22:51:28 +0000 Subject: Add critical options support --- README.md | 2 +- example-server.conf | 2 +- server/signer/signer.go | 62 +++++++++++++++++++++++++------------------- server/signer/signer_test.go | 32 ++++++++++++++++++++--- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bf2e5fc..60a1961 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Supported options: - `signing_key`: string. Path to the signing ssh private key you created earlier. See the [note](#a-note-on-files) on files above. - `additional_principals`: array of string. By default certificates will have one principal set - the username portion of the requester's email address. If `additional_principals` is set, these will be added to the certificate e.g. if your production machines use shared user accounts. - `max_age`: string. If set the server will not issue certificates with an expiration value longer than this, regardless of what the client requests. Must be a valid Go [`time.Duration`](https://golang.org/pkg/time/#ParseDuration) string. -- `permissions`: array of string. Actions the certificate can perform. See the [`-O` option to `ssh-keygen(1)`](http://man.openbsd.org/OpenBSD-current/man1/ssh-keygen.1) for a complete list. +- `permissions`: array of string. Specify the actions the certificate can perform. See the [`-O` option to `ssh-keygen(1)`](http://man.openbsd.org/OpenBSD-current/man1/ssh-keygen.1) for a complete list. e.g. `permissions = ["permit-pty", "permit-port-forwarding", force-command=/bin/ls", "source-address=192.168.0.0/24"]` ## aws AWS configuration is only needed for accessing signing keys stored on S3, and isn't totally necessary even then. diff --git a/example-server.conf b/example-server.conf index 9a20c9d..8d299fa 100644 --- a/example-server.conf +++ b/example-server.conf @@ -29,7 +29,7 @@ ssh { signing_key = "signing_key" # Path to the CA signing secret key additional_principals = ["ec2-user", "ubuntu"] # Additional principals to allow max_age = "720h" # Maximum lifetime of a ssh certificate - permissions = ["permit-pty", "permit-X11-forwarding", "permit-agent-forwarding", "permit-port-forwarding", "permit-user-rc"] # Permissions associated with a certificate + permissions = ["permit-pty", "permit-X11-forwarding", "permit-agent-forwarding", "permit-port-forwarding", "permit-user-rc", "force-command=/bin/ls"] # Permissions associated with a certificate } # Optional AWS config. if an aws config is present, then files (e.g. signing key or tls cert) can be read from S3 using the syntax `/s3/bucket/path/to/signing.key`. diff --git a/server/signer/signer.go b/server/signer/signer.go index a4cf919..2a15849 100644 --- a/server/signer/signer.go +++ b/server/signer/signer.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "fmt" "log" + "strings" "time" "go4.org/wkfs" @@ -16,12 +17,38 @@ import ( "golang.org/x/crypto/ssh" ) +var ( + defaultPermissions = map[string]string{ + "permit-X11-forwarding": "", + "permit-agent-forwarding": "", + "permit-port-forwarding": "", + "permit-pty": "", + "permit-user-rc": "", + } +) + // KeySigner does the work of signing a ssh public key with the CA key. type KeySigner struct { ca ssh.Signer validity time.Duration principals []string - permissions map[string]string + permissions []string +} + +func (s *KeySigner) setPermissions(cert *ssh.Certificate) { + cert.CriticalOptions = make(map[string]string) + cert.Extensions = make(map[string]string) + for _, perm := range s.permissions { + if strings.Contains(perm, "=") { + opt := strings.Split(perm, "=") + cert.CriticalOptions[strings.TrimSpace(opt[0])] = strings.TrimSpace(opt[1]) + } else { + cert.Extensions[perm] = "" + } + } + if len(cert.Extensions) == 0 { + cert.Extensions = defaultPermissions + } } // SignUserKey returns a signed ssh certificate. @@ -35,15 +62,15 @@ func (s *KeySigner) SignUserKey(req *lib.SignRequest, username string) (*ssh.Cer req.ValidUntil = expires } cert := &ssh.Certificate{ - CertType: ssh.UserCert, - Key: pubkey, - KeyId: fmt.Sprintf("%s_%d", username, time.Now().UTC().Unix()), - ValidBefore: uint64(req.ValidUntil.Unix()), - ValidAfter: uint64(time.Now().UTC().Add(-5 * time.Minute).Unix()), + CertType: ssh.UserCert, + Key: pubkey, + KeyId: fmt.Sprintf("%s_%d", username, time.Now().UTC().Unix()), + ValidAfter: uint64(time.Now().UTC().Add(-5 * time.Minute).Unix()), + ValidBefore: uint64(req.ValidUntil.Unix()), + ValidPrincipals: []string{username}, } - cert.ValidPrincipals = append(cert.ValidPrincipals, username) cert.ValidPrincipals = append(cert.ValidPrincipals, s.principals...) - cert.Extensions = s.permissions + s.setPermissions(cert) if err := cert.SignCert(rand.Reader, s.ca); err != nil { return nil, err } @@ -67,23 +94,6 @@ func (s *KeySigner) GenerateRevocationList(certs []*store.CertRecord) ([]byte, e return k.Marshal(rand.Reader) } -func makeperms(perms []string) map[string]string { - if len(perms) > 0 { - m := make(map[string]string) - for _, p := range perms { - m[p] = "" - } - return m - } - return map[string]string{ - "permit-X11-forwarding": "", - "permit-agent-forwarding": "", - "permit-port-forwarding": "", - "permit-pty": "", - "permit-user-rc": "", - } -} - // New creates a new KeySigner from the supplied configuration. func New(conf *config.SSH) (*KeySigner, error) { data, err := wkfs.ReadFile(conf.SigningKey) @@ -102,6 +112,6 @@ func New(conf *config.SSH) (*KeySigner, error) { ca: key, validity: validity, principals: conf.AdditionalPrincipals, - permissions: makeperms(conf.Permissions), + permissions: conf.Permissions, }, nil } diff --git a/server/signer/signer_test.go b/server/signer/signer_test.go index baf00e5..3bbdbf9 100644 --- a/server/signer/signer_test.go +++ b/server/signer/signer_test.go @@ -17,9 +17,10 @@ import ( var ( key, _ = ssh.ParsePrivateKey(testdata.Priv) signer = &KeySigner{ - ca: key, - validity: 12 * time.Hour, - principals: []string{"ec2-user"}, + ca: key, + validity: 12 * time.Hour, + principals: []string{"ec2-user"}, + permissions: []string{"permit-pty", "force-command=/bin/ls"}, } ) @@ -79,3 +80,28 @@ func TestRevocationList(t *testing.T) { t.Errorf("cert %s should not be revoked", cert2.KeyId) } } + +func TestPermissions(t *testing.T) { + t.Parallel() + r := &lib.SignRequest{ + Key: string(testdata.Pub), + ValidUntil: time.Now().Add(1 * time.Hour), + } + cert, err := signer.SignUserKey(r, "gopher1") + if err != nil { + t.Error(err) + } + want := struct { + extensions map[string]string + options map[string]string + }{ + extensions: map[string]string{"permit-pty": ""}, + options: map[string]string{"force-command": "/bin/ls"}, + } + if !reflect.DeepEqual(cert.Extensions, want.extensions) { + t.Errorf("Wrong permissions: wanted: %v got :%v", cert.Extensions, want.extensions) + } + if !reflect.DeepEqual(cert.CriticalOptions, want.options) { + t.Errorf("Wrong options: wanted: %v got :%v", cert.CriticalOptions, want.options) + } +} -- cgit v1.2.3