Skip to content

Commit 8b660c1

Browse files
committed
Don't allow embedded ssh servers
1 parent f20fb67 commit 8b660c1

File tree

6 files changed

+157
-46
lines changed

6 files changed

+157
-46
lines changed

CHANGELOG.SSH.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
## What's New
2+
3+
* Ziti Component Management Access (Experimental)
4+
5+
## Ziti Component Management Access
6+
7+
This release contains an experimental feature allowing Ziti Administrators to allow access to management services for ziti components.
8+
9+
This initial release is focused on providing access to SSH, but other management tools could potentially use the same data pipe.
10+
11+
### Why
12+
13+
Ideally one shouldn't use a system to manage itself. However, it can be nice to have a backup way to access a system, when things
14+
go wrong. This could also be a helpful tool for small installations.
15+
16+
Accessing controllers and routers via the management plane and control plane is bad from a separation of data concerns perspective,
17+
but good from minimizing requirements perspective. To access a Ziti SSH service, An SDK client needs access to the REST API, the
18+
edge router with a control channel connection and links to the public routers. With this solution, only the REST API and the control
19+
channel are needed.
20+
21+
### Security
22+
23+
In order to access a component the following is required:
24+
25+
1. The user must be a Ziti administrator
26+
2. The user must be able to reach the Fabric Management API (which can be locked down)
27+
3. The feature must be enabled on the controller used for access
28+
4. The feature must be enabled on the destination component
29+
5. A destination must be configured on the destination component
30+
6. The destination must be to a port on 127.0.0.1. This can't be used to access external systems.
31+
8. The user must have access to the management component. If SSH, this would be an SSH key or other SSH credentials
32+
9. If using SSH, the SSH server only needs to listen on the loopback interface. So SSH doesn't need to be listening on the network
33+
34+
**Warnings**
35+
1. If you do not intend to use the feature, do not enable it.
36+
2. If you enable the feature, follow best practices for good SSH hygiene (audit logs, locked down permissions, etc)
37+
38+
### What's the Data Flow?
39+
40+
The path for accessing controllers is:
41+
42+
* Ziti CLI to
43+
* Controller Fabric Management API to
44+
* a network service listing on the loopback interface, such as SSH.
45+
46+
The path for accessing routers is:
47+
48+
* Ziti CLI to
49+
* Controller Fabric Management API to
50+
* a router via the control channel to
51+
* a network service listing on the loopback interface, such as SSH.
52+
53+
What does this look like?
54+
55+
Each controller you want to allow access through, must enable the feature.
56+
57+
Example controller config:
58+
59+
```
60+
mgmt:
61+
pipe:
62+
enabled: true
63+
enableExperimentalFeature: true
64+
destination: 127.0.0.1:22
65+
```
66+
67+
Note that if you want to allow access through the controller, but not to the controller itself, you can
68+
leave out the `destination` setting.
69+
70+
The router config is identical.
71+
72+
```
73+
mgmt:
74+
pipe:
75+
enabled: true
76+
enableExperimentalFeature: true
77+
destination: 127.0.0.1:22
78+
```
79+
80+
### SSH Access
81+
82+
If your components are set up to point to an SSH server, you can access them as follows:
83+
84+
85+
```
86+
ziti fabric ssh --key /path/to/keyfile ctrl_client
87+
ziti fabric ssh --key /path/to/keyfile ubuntu@ctrl_client
88+
ziti fabric ssh --key /path/to/keyfile -u ubuntu ctrl_client
89+
```
90+
91+
Using the OpenSSH Client is also supported with the `--proxy-mode` flag. This also opens up access to `scp`.
92+
93+
```
94+
ssh -i ~/.fablab/instances/smoketest/ssh_private_key.pem -o ProxyCommand='ziti fabric ssh router-east-1 --proxy-mode' ubuntu@router-east-1
95+
scp -i ~/.fablab/instances/smoketest/ssh_private_key.pem -o ProxyCommand='ziti fabric ssh ctrl1 --proxy-mode' ubuntu@ctrl1:./fablab/bin/ziti .
96+
```
97+
98+
Note that you must have credentials to the host machine in addition to being a Ziti Administrator.
99+
100+
### Alternate Access
101+
102+
You can use the proxy mode to get a pipe to whatever service you've got configured.
103+
104+
`ziti fabric ssh ctrl1 --proxy-mode`
105+
106+
It's up to you to connect whatever your management client is to that local pipe. Right now it only supports
107+
proxy via the stdin/stdout of the process. Supporting TCP or Unix Domain Socket proxies wouldn't be difficult
108+
if there was use case for them.

common/datapipe/config.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ import (
3131
type LocalAccessType string
3232

3333
const (
34-
LocalAccessTypeNone LocalAccessType = ""
35-
LocalAccessTypePort LocalAccessType = "local-port"
36-
LocalAccessTypeEmbeddedSshServer LocalAccessType = "embedded-ssh-server"
34+
LocalAccessTypeNone LocalAccessType = ""
35+
LocalAccessTypePort LocalAccessType = "local-port"
3736
)
3837

3938
type Config struct {
@@ -53,10 +52,6 @@ func (self *Config) IsLocalPort() bool {
5352
return self.LocalAccessType == LocalAccessTypePort
5453
}
5554

56-
func (self *Config) IsEmbedded() bool {
57-
return self.LocalAccessType == LocalAccessTypeEmbeddedSshServer
58-
}
59-
6055
func (self *Config) LoadConfig(m map[interface{}]interface{}) error {
6156
log := pfxlog.Logger()
6257
if v, ok := m["enabled"]; ok {
@@ -87,33 +82,13 @@ func (self *Config) LoadConfig(m map[interface{}]interface{}) error {
8782
portStr := strings.TrimPrefix(destination, "127.0.0.1:")
8883
port, err := strconv.ParseUint(portStr, 10, 16)
8984
if err != nil {
90-
log.WithError(err).Warn("mgmt.pipe is enabled, but destination not valid. Must be '127.0.0.1:<port>' or 'embedded'")
85+
log.WithError(err).Warn("mgmt.pipe is enabled, but destination not valid; must be '127.0.0.1:<port>'")
9186
self.Enabled = false
9287
return nil
9388
}
9489
self.DestinationPort = uint16(port)
95-
} else if destination == "embedded-ssh-server" {
96-
self.LocalAccessType = LocalAccessTypeEmbeddedSshServer
97-
98-
if v, ok = m["authorizedKeysFile"]; ok {
99-
if keysFile, ok := v.(string); ok {
100-
self.AuthorizedKeysFile = keysFile
101-
} else {
102-
log.Warnf("mgmt.pipe is enabled, but 'embedded' destination configured and authorizedKeysFile configuration is not type string, but %T", v)
103-
self.Enabled = false
104-
return nil
105-
}
106-
}
107-
108-
if v, ok = m["shell"]; ok {
109-
if s, ok := v.(string); ok {
110-
self.ShellPath = s
111-
} else {
112-
log.Warnf("mgmt.pipe is enabled, but 'embedded' destination configured and shell configuration is not type string, but %T", v)
113-
}
114-
}
11590
} else {
116-
log.Warn("mgmt.pipe is enabled, but destination not valid. Must be 'localhost:port' or 'embedded'")
91+
log.Warn("mgmt.pipe is enabled, but destination not valid; must be '127.0.0.1:<port>'")
11792
self.Enabled = false
11893
return nil
11994
}

common/datapipe/ssh.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build !windows
2+
13
/*
24
Copyright NetFoundry Inc.
35

common/datapipe/ssh_windows.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright NetFoundry Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package datapipe
18+
19+
import (
20+
"errors"
21+
"github.com/gliderlabs/ssh"
22+
)
23+
24+
type SshRequestHandler struct {
25+
config *Config
26+
options []ssh.Option
27+
}
28+
29+
func (self *SshRequestHandler) HandleSshRequest(conn *EmbeddedSshConn) error {
30+
return errors.New("ssh connection not supported on windows")
31+
}

controller/handler_mgmt/pipe.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ func (handler *mgmtPipeHandler) HandleReceive(msg *channel.Message, ch channel.C
7272
return
7373
}
7474

75+
if handler.pipe != nil {
76+
handler.respondError(msg, "pipe already established on this endpoint, start a new mgmt connection to start a new pipe")
77+
return
78+
}
79+
7580
if request.DestinationType.CheckControllers() {
7681
log.Infof("checking requested destination '%s' against local id '%s'", request.Destination, handler.network.GetAppId())
7782
if request.Destination == handler.network.GetAppId() {
@@ -181,22 +186,17 @@ func (handler *mgmtPipeHandler) pipeToLocalhost(msg *channel.Message) {
181186
return
182187
}
183188

184-
if cfg.IsEmbedded() {
185-
handler.pipeToEmbeddedSshServer(msg, pipeId)
186-
return
187-
}
188-
189-
log.Error("mgmt.pipe misconfigured, enabled, but neither localPort nor embedded enabled")
189+
log.Error("mgmt.pipe misconfigured, enabled, but no local endpoint configured")
190190
handler.respondError(msg, "server is misconfigured, unable to connect pipe")
191191
}
192192

193193
func (handler *mgmtPipeHandler) pipeToLocalPort(msg *channel.Message, pipeId uint32) {
194194
cfg := handler.registry.GetConfig()
195195
log := pfxlog.ContextLogger(handler.ch.Label()).
196-
WithField("destination", fmt.Sprintf("localhost:%d", cfg.DestinationPort)).
196+
WithField("destination", fmt.Sprintf("127.0.0.1:%d", cfg.DestinationPort)).
197197
WithField("pipeId", pipeId)
198198

199-
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", cfg.DestinationPort))
199+
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", cfg.DestinationPort))
200200
if err != nil {
201201
log.WithError(err).Error("failed to connect mgmt pipe")
202202
handler.respondError(msg, err.Error())
@@ -235,7 +235,7 @@ func (handler *mgmtPipeHandler) pipeToLocalPort(msg *channel.Message, pipeId uin
235235
log.Info("started mgmt pipe to local controller")
236236
}
237237

238-
func (handler *mgmtPipeHandler) pipeToEmbeddedSshServer(msg *channel.Message, pipeId uint32) {
238+
func (handler *mgmtPipeHandler) PipeToEmbeddedSshServer(msg *channel.Message, pipeId uint32) {
239239
log := pfxlog.ContextLogger(handler.ch.Label()).
240240
WithField("destination", "embedded-ssh-server").
241241
WithField("pipeId", pipeId)

router/handler_ctrl/pipe.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,21 +79,16 @@ func (handler *ctrlPipeHandler) HandleReceive(msg *channel.Message, ch channel.C
7979
return
8080
}
8181

82-
if handler.env.GetMgmtPipeConfig().IsEmbedded() {
83-
handler.pipeToEmbeddedSshServer(msg, req)
84-
return
85-
}
86-
8782
log.Error("no configured pipe handler")
8883
handler.respondError(msg, "no configured pipe handler")
8984
}
9085

9186
func (handler *ctrlPipeHandler) pipeToLocalPort(msg *channel.Message, req *ctrl_pb.CtrlPipeRequest) {
9287
log := pfxlog.ContextLogger(handler.ch.Label()).
93-
WithField("destination", fmt.Sprintf("localhost:%d", handler.env.GetMgmtPipeConfig().DestinationPort)).
88+
WithField("destination", fmt.Sprintf("127.0.0.1:%d", handler.env.GetMgmtPipeConfig().DestinationPort)).
9489
WithField("pipeId", req.ConnId)
9590

96-
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", handler.env.GetMgmtPipeConfig().DestinationPort))
91+
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", handler.env.GetMgmtPipeConfig().DestinationPort))
9792
if err != nil {
9893
log.WithError(err).Error("failed to dial pipe destination")
9994
handler.respondError(msg, err.Error())
@@ -126,7 +121,7 @@ func (handler *ctrlPipeHandler) pipeToLocalPort(msg *channel.Message, req *ctrl_
126121
go pipe.readLoop()
127122
}
128123

129-
func (handler *ctrlPipeHandler) pipeToEmbeddedSshServer(msg *channel.Message, req *ctrl_pb.CtrlPipeRequest) {
124+
func (handler *ctrlPipeHandler) PipeToEmbeddedSshServer(msg *channel.Message, req *ctrl_pb.CtrlPipeRequest) {
130125
log := pfxlog.ContextLogger(handler.ch.Label()).
131126
WithField("destination", "embedded-ssh-server").
132127
WithField("pipeId", req.ConnId)

0 commit comments

Comments
 (0)