/
proxy_service.go
178 lines (152 loc) · 4.56 KB
/
proxy_service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package sshagent
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"strconv"
validation "github.com/go-ozzo/ozzo-validation"
"golang.org/x/crypto/ssh/agent"
"github.com/cyberark/secretless-broker/internal"
"github.com/cyberark/secretless-broker/pkg/secretless/log"
)
type proxyService struct {
done bool
listener net.Listener
logger log.Logger
keyring agent.Agent
retrieveCredentials internal.CredentialsRetriever
}
// NewProxyService constructs a new instance of a SSH Agent ProxyService. The
// constructor takes a Listener, Logger and CredentialResolver.
// An SSH Agent ProxyService serves ssh-agent protocol requests from
// an in-memory keyring.
//
// Typical usage has the SSH client delegating auth to the agent e.g.:
// SSH_AUTH_SOCK=/sock/.agent ssh -T git@github.com
//
// NOTE: The keyring is populated at proxy service startup and so is unable to cope
// with rotation
func NewProxyService(
listener net.Listener,
logger log.Logger,
retrieveCredentials internal.CredentialsRetriever,
) (internal.Service, error) {
errors := validation.Errors{}
if retrieveCredentials == nil {
errors["retrieveCredentials"] = fmt.Errorf("retrieveCredentials cannot be nil")
}
if logger == nil {
errors["logger"] = fmt.Errorf("logger cannot be nil")
}
if listener == nil {
errors["listener"] = fmt.Errorf("listener cannot be nil")
}
if err := errors.Filter(); err != nil {
return nil, err
}
return &proxyService{
retrieveCredentials: retrieveCredentials,
listener: listener,
logger: logger,
done: false,
keyring: agent.NewKeyring(),
}, nil
}
func newPrivateKey(pemStr []byte) (interface{}, error) {
block, _ := pem.Decode(pemStr)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("unsupported key type %q", block.Type)
}
}
func (proxy *proxyService) populateKeyring(
backendCredentials map[string][]byte,
) error {
var err error
key := agent.AddedKey{}
// select between rsa and ecdsa
if rsa, ok := backendCredentials["rsa"]; ok {
key.PrivateKey, err = newPrivateKey(rsa)
} else if ecdsa, ok := backendCredentials["ecdsa"]; ok {
key.PrivateKey, err = newPrivateKey(ecdsa)
} else {
err = fmt.Errorf("neither 'rsa' nor 'ecdsa' credentials found")
}
if err != nil {
return err
}
// TODO: neither comment, lifetime, nor confirm is a credential.
// Maybe Handler needs a mechanism for these types of non-secret configuration options.
if comment, ok := backendCredentials["comment"]; ok {
key.Comment = string(comment)
}
if lifetime, ok := backendCredentials["lifetime"]; ok {
var lt uint64
lt, err = strconv.ParseUint(string(lifetime), 10, 32)
if err != nil {
return err
}
key.LifetimeSecs = uint32(lt)
}
if confirm, ok := backendCredentials["confirm"]; ok {
key.ConfirmBeforeUse, err = strconv.ParseBool(string(confirm))
if err != nil {
return err
}
}
return proxy.keyring.Add(key)
}
// Start initiates the net.Listener to listen for incoming connections
func (proxy *proxyService) Start() error {
logger := proxy.logger
logger.Infof("Starting service")
if proxy.done {
return fmt.Errorf("cannot call Start on stopped ProxyService")
}
// TODO: this proxy service only fetches credentials at the outset
// so can not cope with rotation
backendCredentials, err := proxy.retrieveCredentials()
defer internal.ZeroizeCredentials(backendCredentials)
if err != nil {
return fmt.Errorf("failed on retrieve credentials: %s", err)
}
err = proxy.populateKeyring(backendCredentials)
if err != nil {
return err
}
go func() {
for !proxy.done {
conn, err := proxy.listener.Accept()
if err != nil {
logger.Errorf("Failed on accept connection: %s", err)
return
}
go func() {
proxy.logger.Debugf("Serving SSH Agent connection on %v", conn.LocalAddr())
err := agent.ServeAgent(proxy.keyring, conn)
if err != nil && err != io.EOF {
logger.Errorf("Failed on handle connection: %s", err)
return
}
logger.Debugf("Connection closed on %v", conn.LocalAddr())
}()
}
}()
return nil
}
// Stop terminates proxyService by closing the listening net.Listener
func (proxy *proxyService) Stop() error {
proxy.logger.Infof("Stopping service")
proxy.done = true
// TODO: we should verify with a channel that outer goroutine actually stops.
return proxy.listener.Close()
}