diff --git a/README.md b/README.md index ae767cea0e2f426ac6bd9842fee89c858f18fce0..69486664c60cd21612de0efb34c81d1c7a154739 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ For any option that takes a file path as a parameter (e.g. SSH signing key, TLS - `tls_key` : string. Path to the TLS key. See the [note](#a-note-on-files) on files above. - `tls_cert` : string. Path to the TLS cert. See the [note](#a-note-on-files) on files above. - `letsencrypt_servername`: string. If set will request a certificate from LetsEncrypt. This should match the expected FQDN of the server. -- `letsencrypt_cachedir`: string. Directory to cache the LetsEncrypt certificate. +- `letsencrypt_cachedir`: string. Directory to cache the LetsEncrypt certificate. See the [note](#a-note-on-files) on files above. - `address` : string. IP address to listen on. If unset the server listens on all addresses. - `port` : int. Port to listen on. - `user` : string. User to which the server drops privileges to. @@ -223,7 +223,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/cmd/cashierd/main.go b/cmd/cashierd/main.go index f63189c28c8b105ce9b7570a35394aa0ab938daa..2557ab6ed9e502cc7f49d9a11e1cbceb33d6cc76 100644 --- a/cmd/cashierd/main.go +++ b/cmd/cashierd/main.go @@ -25,6 +25,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/sessions" + wkfscache "github.com/nsheridan/autocert-wkfs-cache" "github.com/nsheridan/cashier/lib" "github.com/nsheridan/cashier/server/auth" "github.com/nsheridan/cashier/server/auth/github" @@ -353,7 +354,7 @@ func main() { if conf.Server.LetsEncryptServername != "" { m := autocert.Manager{ Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache(conf.Server.LetsEncryptCache), + Cache: wkfscache.Cache(conf.Server.LetsEncryptCache), HostPolicy: autocert.HostWhitelist(conf.Server.LetsEncryptServername), } tlsConfig.GetCertificate = m.GetCertificate diff --git a/example-server.conf b/example-server.conf index 9a20c9d0209c93126eb95e79fe30ca278b199f8d..8d299fa72504e7d73689036892adac3d79df3d17 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/helpers/vault/vault.go b/server/helpers/vault/vault.go index bec18b98483989701ca1bfc2867ee8c7b51507fd..e522d51d0bbea6424383bdd9a108ded016522731 100644 --- a/server/helpers/vault/vault.go +++ b/server/helpers/vault/vault.go @@ -53,3 +53,10 @@ func (c *Client) Read(value string) (string, error) { } return secret.(string), nil } + +// Delete deletes the secret from vault. +func (c *Client) Delete(value string) error { + p, _ := parseName(value) + _, err := c.vault.Logical().Delete(p) + return err +} diff --git a/server/signer/signer.go b/server/signer/signer.go index a4cf919cea5eedd97c673e786433f5b1f50719d8..2a15849d6e409dea9a4c85cb799ca8ee04e5575a 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 baf00e54142dc73866e2f3a30730ae9d6604eea4..3bbdbf949a2f2f77e80deb503121df423f0dd66c 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) + } +} diff --git a/server/wkfs/vaultfs/vault.go b/server/wkfs/vaultfs/vault.go index f7c136035a01493a95bb6f889db8e8a830ede2e3..dcefd54dfb99e575ff00e6e5fefdbe3783efc180 100644 --- a/server/wkfs/vaultfs/vault.go +++ b/server/wkfs/vaultfs/vault.go @@ -69,6 +69,10 @@ func (fs *vaultFS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileW return nil, errors.New("not implemented") } +func (fs *vaultFS) Remove(path string) error { + return fs.client.Delete(path) +} + type statInfo struct { name string size int64 diff --git a/vendor/github.com/nsheridan/autocert-wkfs-cache/cache.go b/vendor/github.com/nsheridan/autocert-wkfs-cache/cache.go new file mode 100644 index 0000000000000000000000000000000000000000..e829ef243af78d5003e925294e235a695ceb28ab --- /dev/null +++ b/vendor/github.com/nsheridan/autocert-wkfs-cache/cache.go @@ -0,0 +1,85 @@ +package wkfscache + +import ( + "os" + "path/filepath" + + "go4.org/wkfs" + + "golang.org/x/crypto/acme/autocert" + "golang.org/x/net/context" +) + +type Cache string + +// Get reads a certificate data from the specified file name. +func (d Cache) Get(ctx context.Context, name string) ([]byte, error) { + name = filepath.Join(string(d), name) + var ( + data []byte + err error + done = make(chan struct{}) + ) + go func() { + data, err = wkfs.ReadFile(name) + close(done) + }() + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-done: + } + if os.IsNotExist(err) { + return nil, autocert.ErrCacheMiss + } + return data, err +} + +// Put writes the certificate data to the specified file name. +// The file will be created with 0600 permissions. +func (d Cache) Put(ctx context.Context, name string, data []byte) error { + if err := wkfs.MkdirAll(string(d), 0700); err != nil { + return err + } + + done := make(chan struct{}) + var err error + go func() { + defer close(done) + if err := wkfs.WriteFile(filepath.Join(string(d), name), data, 0600); err != nil { + return + } + // prevent overwriting the file if the context was cancelled + if ctx.Err() != nil { + return // no need to set err + } + }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + } + return err +} + +// Delete removes the specified file name. +func (d Cache) Delete(ctx context.Context, name string) error { + name = filepath.Join(string(d), name) + var ( + err error + done = make(chan struct{}) + ) + go func() { + err = wkfs.Remove(name) + close(done) + }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + } + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} diff --git a/vendor/github.com/nsheridan/wkfs/s3/s3.go b/vendor/github.com/nsheridan/wkfs/s3/s3.go index 19e72a999d70ea2a4b2f588cc5726b560d1f5de2..de44f93a082c70042b36807e5bf65abdd67a183a 100644 --- a/vendor/github.com/nsheridan/wkfs/s3/s3.go +++ b/vendor/github.com/nsheridan/wkfs/s3/s3.go @@ -28,6 +28,8 @@ type Options struct { SecretKey string } +var _ wkfs.FileSystem = (*s3FS)(nil) + // Register the /s3/ filesystem as a well-known filesystem. func Register(opts *Options) { if opts == nil { @@ -91,6 +93,12 @@ func (fs *s3FS) Open(name string) (wkfs.File, error) { Key: &fileName, }) if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case "NoSuchKey", "NoSuchBucket": + return nil, os.ErrNotExist + } + } return nil, err } defer obj.Body.Close() @@ -131,7 +139,7 @@ func (fs *s3FS) Lstat(name string) (os.FileInfo, error) { } func (fs *s3FS) MkdirAll(path string, perm os.FileMode) error { - _, err := fs.OpenFile(fmt.Sprintf("%s/", filepath.Clean(path)), os.O_CREATE, perm) + _, err := fs.OpenFile(fmt.Sprintf("%s/", filepath.Clean(path)), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) return err } @@ -154,6 +162,19 @@ func (fs *s3FS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileWrit return NewS3file(bucket, filename, fs.sc) } +func (fs *s3FS) Remove(name string) error { + var err error + bucket, filename, err := fs.parseName(name) + if err != nil { + return err + } + _, err = fs.sc.DeleteObject(&s3.DeleteObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filename), + }) + return err +} + type statInfo struct { name string size int64 diff --git a/vendor/go4.org/wkfs/gcs/gcs.go b/vendor/go4.org/wkfs/gcs/gcs.go index a970c7501aba500c5bccead8f8fc78f648389761..d76882483c67c4a7b69987c16904a9fa43f6371d 100644 --- a/vendor/go4.org/wkfs/gcs/gcs.go +++ b/vendor/go4.org/wkfs/gcs/gcs.go @@ -165,6 +165,14 @@ func (fs *gcsFS) OpenFile(name string, flag int, perm os.FileMode) (wkfs.FileWri return fs.sc.Bucket(bucket).Object(fileName).NewWriter(fs.ctx), nil } +func (fs *gcsFS) Remove(name string) error { + bucket, fileName, err := fs.parseName(name) + if err != nil { + return err + } + return fs.sc.Bucket(bucket).Object(fileName).Delete(fs.ctx) +} + type statInfo struct { name string size int64 diff --git a/vendor/go4.org/wkfs/wkfs.go b/vendor/go4.org/wkfs/wkfs.go index f4df062dc05d8fe22076cb9cb887c92b0451b8f6..08c8786c4a1cfe52fc798c45c11c23ccdd724a4c 100644 --- a/vendor/go4.org/wkfs/wkfs.go +++ b/vendor/go4.org/wkfs/wkfs.go @@ -55,6 +55,7 @@ func MkdirAll(path string, perm os.FileMode) error { return fs(path).MkdirAll(pa func OpenFile(name string, flag int, perm os.FileMode) (FileWriter, error) { return fs(name).OpenFile(name, flag, perm) } +func Remove(name string) error { return fs(name).Remove(name) } func Create(name string) (FileWriter, error) { // like os.Create but WRONLY instead of RDWR because we don't // expose a Reader here. @@ -79,6 +80,7 @@ func (osFS) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(p func (osFS) OpenFile(name string, flag int, perm os.FileMode) (FileWriter, error) { return os.OpenFile(name, flag, perm) } +func (osFS) Remove(name string) error { return os.Remove(name) } type FileSystem interface { Open(name string) (File, error) @@ -86,6 +88,7 @@ type FileSystem interface { Stat(name string) (os.FileInfo, error) Lstat(name string) (os.FileInfo, error) MkdirAll(path string, perm os.FileMode) error + Remove(name string) error } // well-known filesystems diff --git a/vendor/vendor.json b/vendor/vendor.json index 27fa85ebd6f59624384f0c395fcc9bb44bd13246..48a6e98ddd5dd6d9da3f8b838dc9198ca5fd9cd7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -393,10 +393,16 @@ "revisionTime": "2016-12-11T22:23:15Z" }, { - "checksumSHA1": "Ywe06VqOCpwDNjipGTMO0oOG/Yg=", + "checksumSHA1": "hTzdsWWDTWFpX1FcF77fKgR0tEM=", + "path": "github.com/nsheridan/autocert-wkfs-cache", + "revision": "fafece944e938451c2e901fdc355b75f675562f1", + "revisionTime": "2017-01-13T00:09:44Z" + }, + { + "checksumSHA1": "4YKc2c3W7KOIkhSg/InVVbQjqDk=", "path": "github.com/nsheridan/wkfs/s3", - "revision": "60e6f1760f59568e4ce95080d08cd4a90c3c50c7", - "revisionTime": "2016-12-29T20:48:42Z" + "revision": "7e8499ec8b00669d3a0a262273b9342d3c63cb1c", + "revisionTime": "2017-01-12T23:56:57Z" }, { "checksumSHA1": "8Y05Pz7onrQPcVWW6JStSsYRh6E=", @@ -495,16 +501,16 @@ "revisionTime": "2016-07-21T22:16:07Z" }, { - "checksumSHA1": "BS9oue0y6JjMzz3spKlMTVmxZxo=", + "checksumSHA1": "RBe0HvUoZ1JL4XXPxslcvt+E6AI=", "path": "go4.org/wkfs", - "revision": "09d86de304dc27e636298361bbfee4ac6ab04f21", - "revisionTime": "2016-11-18T21:00:15Z" + "revision": "0d03c2721aeea5277882f764f9ac7dd19fdfe4ac", + "revisionTime": "2017-01-01T02:01:48Z" }, { - "checksumSHA1": "VcZWSieqrSxETQY2EP97rg4kLAw=", + "checksumSHA1": "soMi4lOier3JilXADBSxqyNAg2g=", "path": "go4.org/wkfs/gcs", - "revision": "09d86de304dc27e636298361bbfee4ac6ab04f21", - "revisionTime": "2016-11-18T21:00:15Z" + "revision": "0d03c2721aeea5277882f764f9ac7dd19fdfe4ac", + "revisionTime": "2017-01-01T02:01:48Z" }, { "checksumSHA1": "TK1Yr8BbwionaaAvM+77lwAAx/8=", @@ -551,8 +557,8 @@ { "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", "path": "golang.org/x/net/context", - "revision": "45e771701b814666a7eb299e6c7a57d0b1799e91", - "revisionTime": "2016-12-15T19:42:18Z" + "revision": "60c41d1de8da134c05b7b40154a9a82bf5b7edb9", + "revisionTime": "2017-01-10T03:16:11Z" }, { "checksumSHA1": "WHc3uByvGaMcnSoI21fhzYgbOgg=",