Skip to content

Commit

Permalink
Don't allow embedded ssh servers
Browse files Browse the repository at this point in the history
  • Loading branch information
plorenz committed Nov 4, 2024
1 parent ab4d98c commit f77b514
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 56 deletions.
107 changes: 107 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* HA Bootstrap Changes
* Connect Events
* SDK Events
* Ziti Component Management Access (Experimental)
* Bug fixes and other HA work

## New Router Metrics
Expand Down Expand Up @@ -211,6 +212,111 @@ events:
}
```

## Ziti Component Management Access

This release contains an experimental feature allowing Ziti Administrators to allow access to management services for ziti components.

This initial release is focused on providing access to SSH, but other management tools could potentially use the same data pipe.

### Why

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
go wrong. This could also be a helpful tool for small installations.

Accessing controllers and routers via the management plane and control plane is bad from a separation of data concerns perspective,
but good from minimizing requirements perspective. To access a Ziti SSH service, An SDK client needs access to the REST API, the
edge router with a control channel connection and links to the public routers. With this solution, only the REST API and the control
channel are needed.

### Security

In order to access a component the following is required:

1. The user must be a Ziti administrator
2. The user must be able to reach the Fabric Management API (which can be locked down)
3. The feature must be enabled on the controller used for access
4. The feature must be enabled on the destination component
5. A destination must be configured on the destination component
6. The destination must be to a port on 127.0.0.1. This can't be used to access external systems.
8. The user must have access to the management component. If SSH, this would be an SSH key or other SSH credentials
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

**Warnings**
1. If you do not intend to use the feature, do not enable it.
2. If you enable the feature, follow best practices for good SSH hygiene (audit logs, locked down permissions, etc)

### What's the Data Flow?

The path for accessing controllers is:

* Ziti CLI to
* Controller Fabric Management API to
* a network service listing on the loopback interface, such as SSH.

The path for accessing routers is:

* Ziti CLI to
* Controller Fabric Management API to
* a router via the control channel to
* a network service listing on the loopback interface, such as SSH.

What does this look like?

Each controller you want to allow access through, must enable the feature.

Example controller config:

```
mgmt:
pipe:
enabled: true
enableExperimentalFeature: true
destination: 127.0.0.1:22
```

Note that if you want to allow access through the controller, but not to the controller itself, you can
leave out the `destination` setting.

The router config is identical.

```
mgmt:
pipe:
enabled: true
enableExperimentalFeature: true
destination: 127.0.0.1:22
```

### SSH Access

If your components are set up to point to an SSH server, you can access them as follows:


```
ziti fabric ssh --key /path/to/keyfile ctrl_client
ziti fabric ssh --key /path/to/keyfile ubuntu@ctrl_client
ziti fabric ssh --key /path/to/keyfile -u ubuntu ctrl_client
```

Using the OpenSSH Client is also supported with the `--proxy-mode` flag. This also opens up access to `scp`.

```
ssh -i ~/.fablab/instances/smoketest/ssh_private_key.pem -o ProxyCommand='ziti fabric ssh router-east-1 --proxy-mode' ubuntu@router-east-1
scp -i ~/.fablab/instances/smoketest/ssh_private_key.pem -o ProxyCommand='ziti fabric ssh ctrl1 --proxy-mode' ubuntu@ctrl1:./fablab/bin/ziti .
```

Note that you must have credentials to the host machine in addition to being a Ziti Administrator.

### Alternate Access

You can use the proxy mode to get a pipe to whatever service you've got configured.

`ziti fabric ssh ctrl1 --proxy-mode`

It's up to you to connect whatever your management client is to that local pipe. Right now it only supports
proxy via the stdin/stdout of the process. Supporting TCP or Unix Domain Socket proxies wouldn't be difficult
if there was use case for them.

## Component Updates and Bug Fixes

* github.com/openziti/channel/v3: [v3.0.5 -> v3.0.7](https://github.com/openziti/channel/compare/v3.0.5...v3.0.7)
Expand All @@ -231,6 +337,7 @@ events:
* [Issue #2468](https://github.com/openziti/ziti/issues/2468) - enrollment signing cert is not properly identified



# Release 1.1.15

## What's New
Expand Down
33 changes: 4 additions & 29 deletions common/datapipe/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ import (
type LocalAccessType string

const (
LocalAccessTypeNone LocalAccessType = ""
LocalAccessTypePort LocalAccessType = "local-port"
LocalAccessTypeEmbeddedSshServer LocalAccessType = "embedded-ssh-server"
LocalAccessTypeNone LocalAccessType = ""
LocalAccessTypePort LocalAccessType = "local-port"
)

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

func (self *Config) IsEmbedded() bool {
return self.LocalAccessType == LocalAccessTypeEmbeddedSshServer
}

func (self *Config) LoadConfig(m map[interface{}]interface{}) error {
log := pfxlog.Logger()
if v, ok := m["enabled"]; ok {
Expand Down Expand Up @@ -87,33 +82,13 @@ func (self *Config) LoadConfig(m map[interface{}]interface{}) error {
portStr := strings.TrimPrefix(destination, "127.0.0.1:")
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
log.WithError(err).Warn("mgmt.pipe is enabled, but destination not valid. Must be '127.0.0.1:<port>' or 'embedded'")
log.WithError(err).Warn("mgmt.pipe is enabled, but destination not valid; must be '127.0.0.1:<port>'")
self.Enabled = false
return nil
}
self.DestinationPort = uint16(port)
} else if destination == "embedded-ssh-server" {
self.LocalAccessType = LocalAccessTypeEmbeddedSshServer

if v, ok = m["authorizedKeysFile"]; ok {
if keysFile, ok := v.(string); ok {
self.AuthorizedKeysFile = keysFile
} else {
log.Warnf("mgmt.pipe is enabled, but 'embedded' destination configured and authorizedKeysFile configuration is not type string, but %T", v)
self.Enabled = false
return nil
}
}

if v, ok = m["shell"]; ok {
if s, ok := v.(string); ok {
self.ShellPath = s
} else {
log.Warnf("mgmt.pipe is enabled, but 'embedded' destination configured and shell configuration is not type string, but %T", v)
}
}
} else {
log.Warn("mgmt.pipe is enabled, but destination not valid. Must be 'localhost:port' or 'embedded'")
log.Warn("mgmt.pipe is enabled, but destination not valid; must be '127.0.0.1:<port>'")
self.Enabled = false
return nil
}
Expand Down
2 changes: 2 additions & 0 deletions common/datapipe/ssh.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !windows

/*
Copyright NetFoundry Inc.
Expand Down
31 changes: 31 additions & 0 deletions common/datapipe/ssh_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package datapipe

import (
"errors"
"github.com/gliderlabs/ssh"
)

type SshRequestHandler struct {
config *Config
options []ssh.Option
}

func (self *SshRequestHandler) HandleSshRequest(conn *EmbeddedSshConn) error {
return errors.New("ssh connection not supported on windows")
}
18 changes: 9 additions & 9 deletions controller/handler_mgmt/pipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func (handler *mgmtPipeHandler) HandleReceive(msg *channel.Message, ch channel.C
return
}

if handler.pipe != nil {
handler.respondError(msg, "pipe already established on this endpoint, start a new mgmt connection to start a new pipe")
return
}

if request.DestinationType.CheckControllers() {
log.Infof("checking requested destination '%s' against local id '%s'", request.Destination, handler.network.GetAppId())
if request.Destination == handler.network.GetAppId() {
Expand Down Expand Up @@ -181,22 +186,17 @@ func (handler *mgmtPipeHandler) pipeToLocalhost(msg *channel.Message) {
return
}

if cfg.IsEmbedded() {
handler.pipeToEmbeddedSshServer(msg, pipeId)
return
}

log.Error("mgmt.pipe misconfigured, enabled, but neither localPort nor embedded enabled")
log.Error("mgmt.pipe misconfigured, enabled, but no local endpoint configured")
handler.respondError(msg, "server is misconfigured, unable to connect pipe")
}

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

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

func (handler *mgmtPipeHandler) pipeToEmbeddedSshServer(msg *channel.Message, pipeId uint32) {
func (handler *mgmtPipeHandler) PipeToEmbeddedSshServer(msg *channel.Message, pipeId uint32) {
log := pfxlog.ContextLogger(handler.ch.Label()).
WithField("destination", "embedded-ssh-server").
WithField("pipeId", pipeId)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/fatih/color v1.18.0
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
github.com/gaissmai/extnetip v1.1.0
github.com/gliderlabs/ssh v0.1.1
github.com/gliderlabs/ssh v0.3.7
github.com/go-acme/lego/v4 v4.19.2
github.com/go-openapi/errors v0.22.0
github.com/go-openapi/loads v0.22.0
Expand Down Expand Up @@ -106,7 +106,7 @@ require (
github.com/MichaelMure/go-term-text v0.3.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
Expand Down
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
Expand Down Expand Up @@ -183,7 +184,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
Expand All @@ -200,8 +200,9 @@ github.com/gaissmai/extnetip v1.1.0 h1:ZWEPVPUtw1o//CWh69/Eo79gWEKVYB1STrJjpTR4Q
github.com/gaissmai/extnetip v1.1.0/go.mod h1:Ad+qyjy0r98Uc655JzzWoBTzDW29QR4YZDxzHgqhuqM=
github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-acme/lego/v4 v4.19.2 h1:Y8hrmMvWETdqzzkRly7m98xtPJJivWFsgWi8fcvZo+Y=
github.com/go-acme/lego/v4 v4.19.2/go.mod h1:wtDe3dDkmV4/oI2nydpNXSJpvV10J9RCyZ6MbYxNtlQ=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
Expand Down
11 changes: 3 additions & 8 deletions router/handler_ctrl/pipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,16 @@ func (handler *ctrlPipeHandler) HandleReceive(msg *channel.Message, ch channel.C
return
}

if handler.env.GetMgmtPipeConfig().IsEmbedded() {
handler.pipeToEmbeddedSshServer(msg, req)
return
}

log.Error("no configured pipe handler")
handler.respondError(msg, "no configured pipe handler")
}

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

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

func (handler *ctrlPipeHandler) pipeToEmbeddedSshServer(msg *channel.Message, req *ctrl_pb.CtrlPipeRequest) {
func (handler *ctrlPipeHandler) PipeToEmbeddedSshServer(msg *channel.Message, req *ctrl_pb.CtrlPipeRequest) {
log := pfxlog.ContextLogger(handler.ch.Label()).
WithField("destination", "embedded-ssh-server").
WithField("pipeId", req.ConnId)
Expand Down
4 changes: 2 additions & 2 deletions zititest/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ require (
github.com/MichaelMure/go-term-text v0.3.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
Expand Down Expand Up @@ -67,7 +67,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect
github.com/gaissmai/extnetip v1.1.0 // indirect
github.com/gliderlabs/ssh v0.1.1 // indirect
github.com/gliderlabs/ssh v0.3.7 // indirect
github.com/go-acme/lego/v4 v4.19.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
Expand Down
Loading

0 comments on commit f77b514

Please sign in to comment.