Skip to content

Commit 7b90008

Browse files
committed
Enhanced ssh(1) option parsing and further refactoring; add support for allowed_user defaulting
1 parent 6cf6073 commit 7b90008

File tree

7 files changed

+212
-104
lines changed

7 files changed

+212
-104
lines changed

.goreleaser.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ brews:
4646
homepage: https://just.breathe.io/project/vault-ssh-plus/
4747
dependencies:
4848
- name: vault
49+
test: |
50+
system "#{bin}/vssh --version"
51+
install: |
52+
bin.install "vssh"

README.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ An enhanced implementation of [`vault ssh`](https://www.vaultproject.io/docs/com
88
* Automatic and transparent just-in-time delivery of short-lived, signed, single-use `ssh` client keys.
99
* Principal of Least Privilege: by default signed keys only permit the specific options required.
1010
* Significantly lower memory overhead than `vault ssh`.
11+
* Automatic username mapping for roles with a single, fixed entry in `allowed_users` (e.g. `root`, `jenkins`, `ansible`).
1112

1213
## Requirements
1314

@@ -26,30 +27,24 @@ Usage:
2627
vssh [options] destination [command]
2728

2829
Application Options:
29-
--version show version
30+
--version Show version
3031

3132
Vault SSH key signing Options:
32-
--path= Vault SSH Path (default: ssh) [$VAULT_SSH_PATH]
33-
--role= Vault SSH Role (default: default) [$VAULT_SSH_ROLE]
34-
--ttl= Vault SSH Certificate TTL (default: 300) [$VAULT_SSH_TTL]
35-
-P, --public-key= OpenSSH Public RSA Key to sign (default:
36-
~/.ssh/id_rsa.pub) [$VAULT_SSH_PUBLIC_KEY]
37-
--polp Enforce Principal of Least Privilege [$VAULT_SSH_POLP]
33+
--path= Vault SSH Path (default: ssh) [$VAULT_SSH_PATH]
34+
--role= Vault SSH Role (default: default) [$VAULT_SSH_ROLE]
35+
--ttl= Vault SSH Certificate TTL (default: 300) [$VAULT_SSH_TTL]
36+
-P, --public-key= OpenSSH Public RSA Key to sign (default: ~/.ssh/id_rsa.pub) [$VAULT_SSH_PUBLIC_KEY]
3837

3938
Certificate Extensions:
40-
--default-extensions Disable Principal of Least Privilege and request
41-
signer-default extensions [$VAULT_SSH_DEFAULT_EXTENSIONS]
42-
--agent-forwarding Force permit-agent-forwarding extension
43-
[$VAULT_SSH_AGENT_FORWARDING]
44-
--port-forwarding Force permit-port-forwarding extension
45-
[$VAULT_SSH_PORT_FORWARDING]
46-
--no-pty Force disable permit-pty extension [$VAULT_SSH_NO_PTY]
47-
--user-rc Force permit-user-rc extension [$VAULT_SSH_USER_RC]
48-
--x11-forwarding Force permit-X11-forwarding extension
49-
[$VAULT_SSH_X11_FORWARDING]
39+
--default-extensions Disable automatic extension calculation and request signer-default extensions [$VAULT_SSH_DEFAULT_EXTENSIONS]
40+
--agent-forwarding Force permit-agent-forwarding extension [$VAULT_SSH_AGENT_FORWARDING]
41+
--port-forwarding Force permit-port-forwarding extension [$VAULT_SSH_PORT_FORWARDING]
42+
--no-pty Force disable permit-pty extension [$VAULT_SSH_NO_PTY]
43+
--user-rc Enable permit-user-rc extension [$VAULT_SSH_USER_RC]
44+
--x11-forwarding Force permit-X11-forwarding extension [$VAULT_SSH_X11_FORWARDING]
5045

5146
Help Options:
52-
-h, --help Show this help message
47+
-h, --help Show this help message
5348
```
5449

5550
If you need to override the [SSH Client Key Signing](https://www.vaultproject.io/docs/secrets/ssh/signed-ssh-certificates.html#client-key-signing) mountpoint or role, this is most easily achieved by setting the `VAULT_SSH_PATH` and `VAULT_SSH_ROLE` environment variables in your shell rc.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ go 1.12
55
require (
66
github.com/hashicorp/vault/api v1.0.4
77
github.com/jessevdk/go-flags v1.4.0
8-
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
8+
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
99
)

init-dev.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,17 @@ vault write ssh/roles/default - <<-EOH
2424
}
2525
EOH
2626

27+
vault write ssh/roles/root - <<-EOH
28+
{
29+
"key_type": "ca",
30+
"allow_user_certificates": true,
31+
"algorithm_signer": "rsa-sha2-512",
32+
"default_user": "root",
33+
"allowed_users": "root",
34+
"default_extensions": [],
35+
"allowed_extensions": "permit-pty,permit-port-forwarding",
36+
"ttl": "60"
37+
}
38+
EOH
39+
2740
fg

main.go

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,70 +20,63 @@ const (
2020
date = "unknown"
2121
)
2222

23-
var options struct {
24-
Signer signer.Options `group:"Vault SSH key signing Options"`
25-
OpenSSH openssh.Options `group:"OpenSSH ssh(1) Options" hidden:"yes"`
26-
Version func() `long:"version" group:"Help" description:"show version"`
27-
}
23+
func init() {
24+
var options struct {
25+
Signer signer.Options `group:"Vault SSH key signing Options"`
26+
OpenSSH openssh.Options `group:"OpenSSH ssh(1) Options" hidden:"yes"`
27+
Version func() `long:"version" description:"Show version"`
28+
}
2829

29-
func main() {
3030
options.Version = func() {
3131
fmt.Printf("vault-ssh-plus v%s (%s), %s\n", version, commit, date)
3232
os.Exit(0)
3333
}
34+
3435
parser := flags.NewParser(&options, flags.Default)
3536
parser.Usage = "[options] destination [command]"
3637
if _, err := parser.ParseArgs(os.Args[1:]); err != nil {
3738
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
3839
os.Exit(0)
3940
} else {
40-
fmt.Println(err)
4141
os.Exit(1)
4242
}
4343
}
44+
}
45+
46+
func main() {
47+
var (
48+
vaultClient signer.Client
49+
sshClient openssh.Client
50+
err error
51+
)
4452

45-
vaultClient, unparsedArgs, err := signer.ParseArgs(os.Args[1:])
53+
unparsedArgs, err := signer.ParseArgs(&vaultClient, os.Args[1:])
4654
if err != nil {
4755
log.Fatal("[ERROR] parsing vault options: ", err)
4856
}
4957

50-
sshClient, _, err := openssh.ParseArgs(unparsedArgs)
58+
_, err = openssh.ParseArgs(&sshClient, unparsedArgs)
5159
if err != nil {
5260
log.Fatal("[ERROR] parsing ssh options: ", err)
5361
}
5462

5563
if sshClient.Options.LoginName == "" {
56-
currentUser, _ := user.Current()
57-
sshClient.Options.LoginName = currentUser.Username
64+
sshClient.Options.LoginName = getDefaultUser(&vaultClient, &sshClient)
5865
}
5966

6067
controlConnection := sshClient.ControlConnection()
6168

6269
if !controlConnection && sshClient.Options.ControlCommand != "exit" {
63-
if !vaultClient.Options.Extensions.PortForwarding &&
64-
(sshClient.Options.ProxyJump != "" ||
65-
sshClient.Options.DynamicForward != nil ||
66-
sshClient.Options.LocalForward != nil ||
67-
sshClient.Options.RemoteForward != nil) {
68-
vaultClient.Options.Extensions.PortForwarding = true
69-
}
70-
71-
if !vaultClient.Options.Extensions.NoPTY &&
72-
(sshClient.Options.NoPTY ||
73-
(sshClient.Options.ForcePTY == nil && len(sshClient.Options.Positional.RemoteCommand) > 0)) {
74-
vaultClient.Options.Extensions.NoPTY = true
75-
}
76-
77-
if !vaultClient.Options.Extensions.X11Forwarding && sshClient.Options.ForwardX11 {
78-
vaultClient.Options.Extensions.X11Forwarding = true
79-
}
70+
updateRequestExtensions(&vaultClient.Options.Extensions, &sshClient.Options)
8071

8172
signedKey, err := vaultClient.GetSignedKey(sshClient.Options.LoginName)
8273
if err != nil {
8374
log.Fatal("[ERROR] failed to get signed key: ", err)
8475
}
8576

86-
sshClient.SetSignedKey(signedKey)
77+
if err := sshClient.SetSignedKey(signedKey); err != nil {
78+
log.Fatal("[ERROR] invalid certificate: ", err)
79+
}
8780

8881
signedKeyFile, err := sshClient.WriteSignedKeyFile(
8982
filepath.Dir(vaultClient.Options.PublicKey),
@@ -110,10 +103,50 @@ func main() {
110103

111104
func setupExitHandler(fn string) {
112105
s := make(chan os.Signal)
113-
signal.Notify(s, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
106+
signal.Notify(s, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGQUIT)
114107
go func() {
115108
<-s
116109
_ = os.Remove(fn)
117110
os.Exit(0)
118111
}()
119112
}
113+
114+
func getDefaultUser(vaultClient *signer.Client, sshClient *openssh.Client) string {
115+
var loginName string
116+
117+
// if the role only allows a single, fixed user, use it
118+
allowedUser := vaultClient.GetAllowedUser()
119+
if allowedUser != "" {
120+
loginName = allowedUser
121+
sshClient.PrependArgs([]string{"-l", allowedUser})
122+
}
123+
124+
if loginName == "" {
125+
currentUser, _ := user.Current()
126+
loginName = currentUser.Username
127+
}
128+
129+
return loginName
130+
}
131+
132+
func updateRequestExtensions(requestExtensions *signer.Extensions, sshOptions *openssh.Options) {
133+
if !requestExtensions.AgentForwarding && sshOptions.ForwardAgent {
134+
requestExtensions.AgentForwarding = true
135+
} else if requestExtensions.AgentForwarding && sshOptions.NoForwardAgent {
136+
requestExtensions.AgentForwarding = false
137+
}
138+
139+
if !requestExtensions.PortForwarding &&
140+
(sshOptions.ProxyJump != "" || sshOptions.DynamicForward != nil || sshOptions.LocalForward != nil || sshOptions.RemoteForward != nil) {
141+
requestExtensions.PortForwarding = true
142+
}
143+
144+
if !requestExtensions.NoPTY &&
145+
(sshOptions.NoPTY || (sshOptions.ForcePTY == nil && len(sshOptions.Positional.RemoteCommand) > 0)) {
146+
requestExtensions.NoPTY = true
147+
}
148+
149+
if !requestExtensions.X11Forwarding && sshOptions.ForwardX11 {
150+
requestExtensions.X11Forwarding = true
151+
}
152+
}

openssh/openssh.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import (
1212
"syscall"
1313

1414
"github.com/jessevdk/go-flags"
15+
"golang.org/x/crypto/ssh"
1516
)
1617

1718
type Client struct {
1819
Args []string
1920
Options Options
2021
SignedKey string
2122
SignedKeyFile string
23+
Certificate *ssh.Certificate
2224
}
2325

2426
// Options for https://man.openbsd.org/ssh.1
@@ -49,7 +51,7 @@ type Options struct {
4951
MacSpec string `short:"m" description:"Mac Specification"`
5052
NoRemoteCommand bool `short:"N" description:"Do not execute a remote command"`
5153
NullStdin bool `short:"n" description:"Redirect stdin from /dev/null"`
52-
ControlCommand string `short:"O" description:"Send control command"`
54+
ControlCommand string `short:"O" choice:"check" choice:"forward" choice:"cancel" choice:"exit" choice:"stop" description:"Send control command"`
5355
Option []string `short:"o" description:"Override configuration option"`
5456
Port uint16 `short:"p" default:"22" description:"Port"`
5557
QueryOption string `short:"Q" description:"Query supported algorithms"`
@@ -79,25 +81,23 @@ type Positional struct {
7981
}
8082

8183
// ParseArgs parses arguments intended for https://man.openbsd.org/ssh.1
82-
func ParseArgs(args []string) (Client, []string, error) {
83-
var o Client
84+
func ParseArgs(client *Client, args []string) ([]string, error) {
85+
client.Args = args
8486

85-
o.Args = args
86-
87-
parser := flags.NewParser(&o.Options, flags.PassDoubleDash|flags.IgnoreUnknown)
87+
parser := flags.NewParser(&client.Options, flags.PassDoubleDash|flags.IgnoreUnknown)
8888
unparsedArgs, err := parser.ParseArgs(args)
8989
if err != nil {
90-
return Client{}, nil, err
90+
return nil, err
9191
}
9292

93-
if err := o.ParseDestination(o.Options.Positional.Destination); err != nil {
94-
return Client{}, nil, err
93+
if err := client.ParseDestination(client.Options.Positional.Destination); err != nil {
94+
return nil, err
9595
}
96-
if err := o.ParseOptions(o.Options.Option, "="); err != nil {
97-
return Client{}, nil, err
96+
if err := client.ParseOptions(client.Options.Option, "="); err != nil {
97+
return nil, err
9898
}
9999

100-
return o, unparsedArgs, nil
100+
return unparsedArgs, nil
101101
}
102102

103103
// ParseDestination parses the `destination` argument as an `ssh://` scheme URI and updates options according to what it finds
@@ -130,8 +130,9 @@ func (c *Client) ParseDestination(destination string) error {
130130
// ParseOptions parses ssh_config options of the form `key[separator]value` and updates other options accordingly
131131
func (c *Client) ParseOptions(options []string, separator string) error {
132132
for _, option := range options {
133-
split := strings.Split(option, separator)
134-
key, value := split[0], strings.Join(split[1:], separator)
133+
split := strings.SplitN(option, separator, 2)
134+
key, value := split[0], split[1]
135+
135136
switch key {
136137
case "User":
137138
if c.Options.LoginName == "" {
@@ -169,8 +170,12 @@ func (c *Client) PrependArgs(args []string) {
169170
}
170171

171172
// SetSignedKey sets the Client's signed key
172-
func (c *Client) SetSignedKey(key string) {
173+
func (c *Client) SetSignedKey(key string) error {
173174
c.SignedKey = key
175+
if err := c.ParseSignedKey(); err != nil {
176+
return err
177+
}
178+
return nil
174179
}
175180

176181
// WriteSignedKeyFile updates the signed key at path/name
@@ -214,3 +219,19 @@ func (c *Client) Connect() error {
214219
return cmd.Run()
215220
}
216221
}
222+
223+
func (c *Client) ParseSignedKey() error {
224+
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(c.SignedKey))
225+
if err != nil {
226+
return err
227+
}
228+
229+
cert, ok := pub.(*ssh.Certificate)
230+
if !ok {
231+
return nil // XXX: return an appropriate custom error!
232+
}
233+
234+
c.Certificate = cert
235+
236+
return nil
237+
}

0 commit comments

Comments
 (0)