diff --git a/README.md b/README.md
index b4f356dd9f1d539043b80b05c88e16542c651594..60a196130f1dceb5058117ca247ed830be1ac772 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.
@@ -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/cmd/cashierd/main.go b/cmd/cashierd/main.go
index fb67a36685f2e8077b2d587fd3eb10b4592987bc..83627ad6d0566684a75aafa3012aa133e0447764 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"
@@ -352,7 +353,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=",