Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for accessing ziti components over the mgmt/ctrl channels. Fixes #2439 #2440

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
152 changes: 152 additions & 0 deletions common/datapipe/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
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 (
"fmt"
"github.com/gliderlabs/ssh"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/identity"
gossh "golang.org/x/crypto/ssh"
"os"
"path"
"strconv"
"strings"
)

type LocalAccessType string

const (
LocalAccessTypeNone LocalAccessType = ""
LocalAccessTypePort LocalAccessType = "local-port"
)

type Config struct {
Enabled bool
LocalAccessType LocalAccessType // values: 'none', 'localhost:port', 'embedded'
DestinationPort uint16
AuthorizedKeysFile string
HostKey gossh.Signer
ShellPath string
}

func (self *Config) IsLocalAccessAllowed() bool {
return self.Enabled && self.LocalAccessType != LocalAccessTypeNone
}

func (self *Config) IsLocalPort() bool {
return self.LocalAccessType == LocalAccessTypePort
}

func (self *Config) LoadConfig(m map[interface{}]interface{}) error {
log := pfxlog.Logger()
if v, ok := m["enabled"]; ok {
if enabled, ok := v.(bool); ok {
self.Enabled = enabled
} else {
self.Enabled = strings.EqualFold("true", fmt.Sprintf("%v", v))
}
}
if v, ok := m["enableExperimentalFeature"]; ok {
if enabled, ok := v.(bool); ok {
if !enabled {
self.Enabled = false
}
} else if !strings.EqualFold("true", fmt.Sprintf("%v", v)) {
self.Enabled = false
}
} else {
self.Enabled = false
}

if self.Enabled {
log.Infof("mgmt.pipe enabled")
if v, ok := m["destination"]; ok {
if destination, ok := v.(string); ok {
if strings.HasPrefix(destination, "127.0.0.1:") {
self.LocalAccessType = LocalAccessTypePort
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>'")
self.Enabled = false
return nil
}
self.DestinationPort = uint16(port)
} else {
log.Warn("mgmt.pipe is enabled, but destination not valid; must be '127.0.0.1:<port>'")
self.Enabled = false
return nil
}
}
} else {
self.Enabled = false
log.Warn("mgmt.pipe is enabled, but destination not specified. mgmt.pipe disabled.")
return nil
}
} else {
log.Infof("mgmt.pipe disabled")
}
return nil
}

func (self *Config) NewSshRequestHandler(identity *identity.TokenId) (*SshRequestHandler, error) {
if self.HostKey == nil {
signer, err := gossh.NewSignerFromKey(identity.Cert().PrivateKey)
if err != nil {
return nil, err
}
self.HostKey = signer
}

keysFile := self.AuthorizedKeysFile
if keysFile == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("could not set up ssh request handler, failing get home dir, trying to load default authorized keys (%w)", err)
}
keysFile = path.Join(homeDir, ".ssh", "authorized_keys")
}

keysFileContents, err := os.ReadFile(keysFile)
if err != nil {
return nil, fmt.Errorf("could not set up ssh request handler, failed to load authorized keys from '%s' (%w)", keysFile, err)
}

authorizedKeys := map[string]struct{}{}
entryIdx := 0
for len(keysFileContents) > 0 {
pubKey, _, _, rest, err := gossh.ParseAuthorizedKey(keysFileContents)
if err != nil {
return nil, fmt.Errorf("could not set up ssh request handler, failed to load authorized key at index %d from '%s' (%w)", entryIdx, keysFile, err)
}

authorizedKeys[string(pubKey.Marshal())] = struct{}{}
keysFileContents = rest
entryIdx++
}

publicKeyOption := ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
_, found := authorizedKeys[string(key.Marshal())]
return found
})

return &SshRequestHandler{
config: self,
options: []ssh.Option{publicKeyOption},
}, nil
}
Loading
Loading