美文网首页
为容器构建自定义SSH服务

为容器构建自定义SSH服务

作者: 豆腐匠 | 来源:发表于2020-08-17 14:42 被阅读0次

原文链接

在我的职业生涯中,我在网络托管业务中有多个项目。被工程赋予了自豪感的我始终希望给予客户的最大自由度,他们可以自由地做他们想做的事情。早在2009年,我领导的团队就提供了完整的SSH访问权限来维护其站点。那时,甚至FTP加密都很罕见。但是,提供SSH访问并非没有挑战。

我已经编写了启动容器的功能齐全的SSH微服务。GitHub上也提供了这篇文章的示例代码

[站外图片上传中...(image-1c490f-1597646525583)]

传统上提供SSH的Web托管环境是通过提供按站点或按客户的环境来实现的。这些环境通过为每个环境创建单独的彼此分离的Linux用户。如果每个站点都有自己的环境,则此方法效果很好,但是当客户在同一环境中托管多个站点时,则存在危险。如果这些站点中的任何一个受到感染,则存在交叉污染的风险。

两种方法都有一个明显的缺点:无法选择允许用户从单个SSH用户访问一组特定的站点。用户可以看到一个环境中的所有站点,也可以看到一个站点。没有使用户看到特定站点集的选项。

理想情况下的访问。

使用容器

传统方法使用Linux系统用户将站点彼此分开,因此由于权限问题,几乎不可能创建可以访问多个站点的SSH用户。那么如果我们采用 “新”技术来解决这个问题呢?

你知道我要如何处理,这并不新鲜。在Linux上,容器已经存在15年了,在其他操作系统上甚至更长。但是,直到最近才达到广泛采用和普遍使用的阶段。

[站外图片上传中...(image-de7eb4-1597646525583)]

如果我们将每个站点放在一个容器中而不是创建单独的用户呢?如果,PHP,Python或你的网站在容器中运行的并且挂载到容器所在的数据目录呢?

如果你是DockerKubernetes的用户,这看似微不足道,但重要的是要强调在短时间内已经发展了多少技术。早在2009-2010年,安装点数就达到了1万个,这意味着服务器重新启动花费了20至30分钟。

除了技术,这如何解决我们的SSH问题?好吧,你会看到,使用这种方法,站点是由容器而不是Linux系统用户分隔的。从主机操作系统的角度来看,所有站点都以同一用户身份运行,因此所有文件将归同一用户所有。

这使我们能够为每个用户创建一个特殊的容器。这个特殊的容器仅安装有问题的,用户有权访问的站点。用户打开SSH会话时,他们应该进入该容器,而不能访问其他容器。由于实际的运行时环境在其他容器中运行,因此用户也不会意外终止正在运行的Web服务器。干净整洁。

破解SSH守护程序

在过去6到7年的时间里,上述方法仅仅是一种理论。虽然我在2011年编写了使用 OpenVZ容器(或称为虚拟环境)的概念,后来又实现了该概念,但SSH访问仍然是一个困难的话题。

Python是编写SSH服务器的主要语言,SSH服务器将根据用户输入的用户名来代理连接,但该项目从未成功。如果放在今天我使用Python用于此目的,我将研究paramiko之类的库。

输入:Apache Mina

停顿了几年之后我在2017年又开始涉足网络托管业务,并决心解决这个问题。幸运的是我碰到了一些出色的Java开发人员,这些开发人员促进了我对Apache Mina的了解。

Apache Mina提供了使用纯Java环境创建SSH(和FTP)服务器的功能。Mina SSHD允许定义处理程序和用户认证的钩子。这样做不需要操作系统级别的用户。整个连接处理可​​以完全用Java完成。

这为我提供了一个机会:如果我可以将SSH服务器与 Docker API结合使用,那么我可以将通过SSH连接获得的所有信息直接发送给Docker API,反之亦然。实际上,我可以绕过常规的SSH守护程序。

令我惊讶的是它的工作原理。实现PasswordAuthenticatorPublickeyAuthenticator 允许我为用户提供自定义身份验证,而编写DockerizedCommand实现Command接口的类使我可以启动Docker容器。 ContainerAttach操作允许我获取任何通过SSH通道来的数据,反之亦然。

但是,Java实现并非没有问题。当时存在的Docker库不完整,并且使用了与Apache Mina不同的异步IO模型。当用户启动SFTP会话下载数据时,我的SSH服务器将尽快从Docker API中提取数据并将其推送到SSH通道中。然后它将静静等待用户从内存中完成数据下载。

在极少数情况下,用户会耗尽服务器上的4 GB RAM,从而导致崩溃。当然,添加更多的RAM很有帮助,但是这绝对不是理想的解决方案。

用Go重写

当我最近开始学习Go时,我发现扩展标准库包含一个 功能齐全的SSH库。这为我提供了一个完美的学习项目:在Go中重新实现SSH服务器。

注意:你可以从GitHub中获取本文中的代码。请注意,它强调学习。如果你希望看到更适合生产的版本,请查看我的ContainerSSH项目。

首先,我们从一个简单易用的TCP服务器开始。

func main() {
    listener, err := net.Listen(
        "tcp",
        "0.0.0.0:2222",
    );
    if err != nil {
        log.Fatalf(
            "Failed to listen on port 2222 (%s)",
            err,
        )
    }
    log.Printf("Listening on 0.0.0.0:2222")
}

容易吧?现在,该Listen仅打开一个监听套接字,但不接受连接。这就是Accept调用的目的。Accept调用将阻塞,直到有新的连接进入。

func main() {
    //...
    for {
        tcpConn, err := listener.Accept()
        if err != nil {
            log.Printf(
                "Failed to accept (%s)",
                err,
            )
            //Continue with the next loop
            continue
        }
    }
}

建立SSH连接

到目前为止,我们已经可以正常使用TCP连接,但是没有进行SSH协议解码。这就是接下来要做的事情:初始化SSH连接。在此之前,我们需要构建一个SSH配置。首先让我们添加一个密码验证方法:

func main() {
    //...
    sshConfig := &ssh.ServerConfig{}

    // region SSH authentication
    sshConfig.PasswordCallback = func(
        conn ssh.ConnMetadata,
        password []byte,
    ) (
        *ssh.Permissions,
        error,
    ) {
        if conn.User() == "foo" &&
            string(password) == "bar" {
            return &ssh.Permissions{}, nil
        } else {
            return nil, fmt.Errorf(
                "authentication failed",
            )
        }
    }
    //endregion

    for {
        //The previously written Accept code here
    }
}

PasswordCallbackPublicKeyCallback允许使用数据库中的密码和公钥认证。接下来,我们必须创建一个主机密钥。在Linux系统上,可以使用ssh-keygen -t rsa。密钥的加载过程如下所示:

func main() {
   //...
   sshConfig := &ssh.ServerConfig{}
   //...

   // region Host key
   hostKeyData, err := ioutil.ReadFile(
       "ssh_host_rsa_key",
   )
   if err != nil {
       log.Fatalf(
           "failed to load host key (%s)",
           err,
       )
   }
   signer, err := ssh.ParsePrivateKey(
       hostKeyData,
   )
   if err != nil {
       log.Fatalf(
           "failed to parse host key (%s)",
           err,
       )
   }
   sshConfig.AddHostKey(signer)
   // endregion

   for {
       //The previously written Accept code here
   }
}

现在我们可以接受SSH连接了:

func main() {
    //...
    for {
        //...
        sshConn, chans, reqs, err := ssh.NewServerConn(
            tcpConn,
            sshConfig,
        )
        if err != nil {
            log.Printf(
                "handshake failed (%s)",
                err,
            )
            continue
        }
    }
}

这将执行SSH握手并建立安全连接。它将返回很多东西:


SSH连接剖析
  • sshConn 是实际的SSH连接。
  • chans 是一个Go通道,引入了新的SSH通道。一个SSH连接可以具有多个SSH通道,在同一SSH连接中可以处理不同种类的并行数据传输。
  • reqs 是一个处理请求进入的Go通道。请求是客户端请求更改某些内容的方法。
  • err 当然是错误(如果发生)。

Go channels: Go具有一个非常高效的称为goroutines的并发编程模型。Go通道是在这些goroutine之间发送数据的一种方式。你可以将它们想象为应用程序内消息队列。

至此,我们已经建立了SSH连接,我们需要处理请求。首先,我们将拒绝所有全局请求。全局请求将用于请求例如我们不支持的端口转发。

func main() {
    //...
    for {
        //...
        //Reject all global requests.
        //Run this in a goroutine so it
        //doesn't block.
        go ssh.DiscardRequests(reqs)
    }
}

接下来,我们需要处理传入的通道。等待通道将在goroutine中处理,因此我们不会阻塞主程序的执行。当通道进入时,我们将创建另一个goroutine来处理该特定通道。

func main() {
    //...
    for {
        //...
        //Reject all global requests.
        //Run this in a goroutine so it
        //doesn't block.
        go handleChannels(sshConn, chans)
    }
}

func handleChannels(
    conn *ssh.ServerConn,
    chans <-chan ssh.NewChannel,
) {
    for newChannel := range chans {
        go handleChannel(conn, newChannel)
    }
}

func handleChannel(conn *ssh.ServerConn, newChannel ssh.NewChannel) {
    //Handle new channel here
}

处理SSH通道

handleChannel函数中,我们有两个选择:我们接受通道还是拒绝通道。首先,我们的SSH服务器仅支持session通道类型,因此我们拒绝其他所有内容:

//...
func handleChannel(
    conn *ssh.ServerConn,
    newChannel ssh.NewChannel,
) {
    if t := newChannel.ChannelType();
        t != "session" {
        _ = newChannel.Reject(
            ssh.UnknownChannelType,
            fmt.Sprintf(
                "unknown channel type: %s",
                t,
            ),
        )
        return
    }
}

接下来,我们需要创建与Docker引擎的连接。我们将使用以下github.com/docker/docker/client 执行此操作:

//...
func handleChannel(
    conn *ssh.ServerConn,
    newChannel ssh.NewChannel,
) {
    //...
    docker, err := client.NewClient(
        "tcp://127.0.0.1:2375",
        "",
        nil,
        make(map[string]string),
    )
    if err != nil {
        _ = newChannel.Reject(
            ssh.ConnectionFailed,
            fmt.Sprintf(
                "error contacting backend (%s)",
                err,
            ),
        )
        return
    }
}

到目前为止,一切都很好。现在,我们需要定义几个变量,稍后将使用它们:

//...
type channelProperties struct {
    // Allocate pseudo-terminal for
    // interactive sessions.
    pty bool
    // Store the container ID
    // once it is started.
    containerId string
    // Environment variables passed
    // from the SSH session.
    env map[string]string
    // Horizontal screen size
    cols uint
    // Vertical screen size
    rows uint
    // Context required by the Docker client.
    ctx context.Context
    // Docker client
    docker *client.Client
}

func handleChannel(
    conn *ssh.ServerConn,
    newChannel ssh.NewChannel,
) {
    //...
    channelProps := &channelProperties{
        pty:         false,
        containerId: "",
        env:         map[string]string{},
        cols:        80,
        rows:        25,
        ctx:         context.Background(),
        docker:      docker,
    }
}

最后,让我们接受通道。如果通道接受失败,我们将关闭Docker连接并从通道处理中返回。

//...
func handleChannel(
    conn *ssh.ServerConn,
    newChannel ssh.NewChannel,
) {
    //...
    connection, requests, err :=
        newChannel.Accept()
    if err != nil {
        log.Printf(
            "could not accept channel (%s)",
            err,
        )
        err := docker.Close()
        if err != nil {
            log.Printf(
                "error while closing (%s)",
                err,
            )
        }
        return
    }
}

特定于渠道的请求

建立通道后,客户端现在可以发送数据和特定于通道的请求。特定于渠道的请求可以是任何数量,包括自定义的事物。我们将讨论以下内容:

env
设置环境变量。

pty-req
为交互式会话分配伪终端。(你需要使用它来移动光标。)

window-change
更改窗口大小。

shell
执行默认的shell。

由于复杂性,我们将不介绍以下请求类型。你可以查看 ContainerSSH的源代码获取详细信息。

exec
执行自定义程序。

subsystem
启动子系统,例如SFTP。

signal
向进程发送信号。

因此,让我们来实现我们的通道处理程序:

//...
func handleChannel(
    conn *ssh.ServerConn,
    newChannel ssh.NewChannel,
) {
    //...
    removeContainer := func() {
        if channelProps.containerId != "" {
            //Remove container
            removeOptions :=
                types.ContainerRemoveOptions{
                    Force: true,
                }
            err := docker.ContainerRemove(
                channelProps.ctx,
                channelProps.containerId,
                removeOptions,
            )
            if err != nil {
                log.Printf(
                    "error while removing (%s)",
                    err,
                )
            }
            channelProps.containerId = ""
        }
    }
    closeConnections := func() {
        removeContainer()
        //Close Docker connection
        err = docker.Close()
        if err != nil {
            log.Printf(
                "error while closing Docker (%s)",
                err,
            )
        }
        //Close SSH connection
        err := conn.Close()
        if err != nil {
            log.Printf(
                "error while closing SSH (%s)",
                err,
            )
        }
    }

    go func() {
        for req := range requests {
            reply := func(
                success bool,
                message []byte,
            ) {
                if req.WantReply {
                    err := req.Reply(
                        success,
                        message,
                    )
                    if err != nil {
                        closeConnections()
                    }
                }
            }

            handleRequest(
                channel,
                req,
                reply,
                closeConnections,
                removeContainer,
                channelProps
            )
        }
    }()
}

我们基本上是在创建另一个 goroutine来处理通道请求,并且处理将在名为handleRequest的单独函数中完成。如果需要,请求处理程序可以选择答复请求。我们还定义了一个称为closeConnections的函数,如果出现问题或需要自然关闭连接时将调用该函数。

//TODO

实现请求处理程序

作为难题的最后一部分,让我们实现handleRequest功能。首先让我们实现一个拒绝所有请求的默认类型:

//...
type envRequestMsg struct {
    Name  string
    Value string
}

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    default:
        reply(
            false,
            []byte(fmt.Sprintf(
                "unsupported request type (%s)",
                req.Type,
            )),
        )
    }
}

非常简单,让我们继续进行env请求类型。这个用来设置环境变量:

//...
type envRequestMsg struct {
    Name  string
    Value string
}

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    case "env":
        if channelProps.containerId != "" {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "cannot set env variables",
                ),
            ))
            return
        }
        request := envRequestMsg{}
        err := ssh.Unmarshal(req.Payload, request)
        if err != nil {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "invalid payload (%s)",
                    err,
                )),
            )
            return
        }
        channelProps.env[request.Name] =
            request.Value
    default:
        //...
    }
}

如你所见,我们声明了一个envRequestMsg结构。该结构是Payload请求部分的格式,将使用ssh.Unmarshal()函数调用进行解码。收到的环境变量将存储在env变量中。

既然这很容易,让我们实现pty-req用于处理交互式终端的类型:

//...
func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    case "env":
        //...
    case "pty-req":
        if channelProps.containerId != "" {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "cannot set pty after shell",
                )),
            )
            return
        }
        channelProps.pty = true
    default:
        //...
    }
}

更简单的是,我们只是在收到PTY请求时设置一个布尔值。在深入研究丑陋的比特符之前,让我们快速实现window-change处理程序:

type windowChangeRequestMsg struct {
    Columns uint32
    Rows    uint32
    Width   uint32
    Height  uint32
}

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    case "env":
        //...
    case "pty-req":
        //...
    case "window-change":
        request := windowChangeRequestMsg{}
        err := ssh.Unmarshal(req.Payload, request)
        if err != nil {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "invalid payload (%s)",
                    err,
                )),
            )
            return
        }
        channelProps.cols = uint(request.Columns)
        channelProps.rows = uint(request.Rows)
        if channelProps.containerId != "" {
            err = channelProps.
                docker.
                ContainerResize(
                    channelProps.ctx,
                    channelProps.containerId,
                    types.ResizeOptions{
                        Height: channelProps.rows,
                        Width:  channelProps.cols,
                    },
                )
            if err != nil {
                reply(
                    false,
                    []byte(fmt.Sprintf(
                        "failed to set wnd (%s)",
                        err,
                    )),
                )
                return
            }
        }
    default:
        //...
    }
}

如你所见,我们已经开始与Docker API进行交互。当客户端(例如PuTTY)窗口大小更改时,我们还将新尺寸发送到容器。在midnightcommander类的软件以PTY模式运行时,这很有用, 因为它需要正确的尺寸才能缩放到整个窗口。

启动容器

在让我们的全新SSH服务器试用之前,我们还有最后一个任务:实现shell请求。该请求将启动Docker容器并将该容器连接到SSH输入/输出。这将使我们能够实际使用SSH。

作为启动容器的第一步,我们将需要提取要运行的映像:

type windowChangeRequestMsg struct {
    Columns uint32
    Rows    uint32
    Width   uint32
    Height  uint32
}

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    case "env":
        //...
    case "pty-req":
        //...
    case "window-change":
        //...
    case "shell":
        if channelProps.containerId != "" {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "cannot launch a second shell"
                )),
            )
            break
        }
        pullReader, err := channelProps.
            docker.
            ImagePull(
                channelProps.ctx,
                "docker.io/library/busybox",
                types.ImagePullOptions{},
            )
        if err != nil {
            reply( 
                false,
                []byte(fmt.Sprintf(
                    "could not pull busybox (%s)",
                    err,
                )),
            )
            return
        }
        _, err = ioutil.ReadAll(pullReader)
        if err != nil {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "could not pull busybox (%s)",
                    err,
                )),
            )
            return
        }
        err = pullReader.Close()
        if err != nil {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "could not pull busybox (%s)",
                    err,
                )),
            )
            return
        }
    default:
        //...
    }
}

现在我们可以确保预期的目标镜像在本地可用,并且我们可以创建容器:

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    //...
    case "shell":
        //...
        var env []string
        for key, value := range channelProps.env {
            env = append(
                env,
                fmt.Sprintf(
                    "%s=%s",
                    key,
                    value,
                ),
            )
        }
        body, err := channelProps.
            docker.
            ContainerCreate(
                channelProps.ctx,
                &container.Config{
                    Image: "busybox",
                    AttachStdout: true,
                    AttachStderr: true,
                    AttachStdin: true,
                    Tty: channelProps.pty,
                    StdinOnce: true,
                    OpenStdin: true,
                    Env: env,
                },
                &container.HostConfig{},
                &network.NetworkingConfig{},
                "",
            )
        if err != nil {
            reply(
                false,
                []byte(fmt.Sprintf(
                    "failed to launch (%s)",
                    err,
                )),
            )
            return
        }
        channelProps.containerId = body.ID
    default:
        //...
    }
}

容器已创建,因此让我们在开始之前准备附件:

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    //...
    case "shell":
        //...
        attachResult, err := channelProps.
            docker.
            ContainerAttach(
                channelProps.ctx,
                channelProps.containerId,
                types.ContainerAttachOptions{
                    Logs:   true,
                    Stdin:  true,
                    Stderr: true,
                    Stdout: true,
                    Stream: true,
                },
            )
        if err != nil {
            removeContainer()
            reply(
                false,
                []byte(fmt.Sprintf(
                    "failed to attach (%s)",
                    err,
                )),
            )
            return
        }
    default:
        //...
    }
}

完成启动后,只需一个简单的API调用即可:

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    //...
    case "shell":
        //...
        err = channelProps.docker.ContainerStart(
            channelProps.ctx,
            channelProps.containerId,
            types.ContainerStartOptions{},
        )
        if err != nil {
            removeContainer()
            reply(
                false,
                []byte(fmt.Sprintf(
                    "failed to launch (%s)",
                    err,
                )),
            )
            return
        }
    default:
        //...
    }
}

在开始与容器之间来回传输数据之前,我们必须根据之前的window-change请求设置窗口大小 :

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    //...
    case "shell":
        //...
        err = channelProps.
            docker.
            ContainerResize(
                channelProps.ctx,
                channelProps.containerId,
                types.ResizeOptions{
                    Height: channelProps.rows,
                    Width:  channelProps.cols,
                },
            )
        if err != nil {
            removeContainer()
            reply(
                false,
                []byte(fmt.Sprintf(
                    "failed to resize (%s)",
                    err,
                )),
            )
            return
        }
    default:
        //...
    }
}

就是这样!容器正在运行并已连接,窗口具有正确的大小,设置了环境变量。

func handleRequest(
    channel ssh.Channel,
    req *ssh.Request,
    reply func(success bool, message []byte),
    closeConnections func(),
    removeContainer func(),
    channelProps * channelProperties,
) {
    switch req.Type {
    //...
    case "shell":
        //...
        var once sync.Once
        if channelProps.pty {
            go func() {
                _, _ = io.Copy(
                    channel,
                    attachResult.Reader,
                )
                once.Do(closeConnections)
            }()
        } else {
            go func() {
                //Demultiplex Docker stream
                //into stdout/stderr
                _, _ = stdcopy.StdCopy(
                    channel,
                    channel.Stderr(),
                    attachResult.Reader,
                )
                once.Do(closeConnections)
            }()
        }
        go func() {
            _, _ = io.Copy(
                attachResult.Conn,
                channel,
            )
            once.Do(closeConnections)
        }()
    default:
        //...
    }
}

这个需要一点解释。如果容器以交互方式运行,则来自应用程序的所有内容都会以二进制形式通过stdout传递。但是,如果应用程序非交互运行,则SSH通道希望数据通过stdout和stderr的两个独立流来传递。另一方面,Docker仍将使用多路复用的返回格式,因此我们需要使用stdcopy.StdCopy多路复用将流复用为两个单独的流。

最后,当其中一个流结束时,我们使用先前定义的closeConnection函数关闭所有内容。

测试

全部关键代码大约300行,因此现在是时候使用我们全新的SSH服务器了。在第一个终端上:

$ go run main.go
2020/06/09 15:50:31 Listening on 0.0.0.0:2222

在第二个:

$ ssh localhost -l foo -p 2222
foo@localhost's password:  <--- enter "bar" here
/ #

我们有一个正在运行的SSH服务器!

如开头所述,你可以从GitHub上获取完整的代码,如果你想使用execsignal和SFTP以及更多结构化的代码库进行更完整的实现,请查看我的ContainerSSH项目

适配Kubernetes

到目前为止,我们仅讨论了Docker,但是Kubernetes无疑是容器协调竞赛的赢家。值得庆幸的是,Kubernetes API还具有类似于Docker工作原理的附加功能。这里我将把它留给玩Kubernetes SDK的玩家,期待你可以弄清楚如何在你的SSH服务器中实现它。

可用于生产环境吗?

毫无疑问,在谈到SSH安全性时我们会想到Go中的SSH实施足够安全吗?便于维护吗?

事实是,我不知道。当我尝试设置 Mozilla建议的密码套件时我立即遇到了崩溃。我现在已经提交了针对该问题的补丁程序,最终该崩溃并未导致安全问题,但是缺少基本配置检查的事实也令人担忧。

基础坚实, 支持的密码,密钥交换算法和MAC列表 似乎足以提供安全的连接,但我的直觉是该库需要成熟一点才能被考虑用于生产。

用例

创建一个可以启动容器的自定义SSH服务器远远超出了简单的Web托管。想象一所学校,学生需要进入临时环境,或Linux /安全教育环境。这些按需激发工作负载的情况基本上就是云和Kubernetes的目的,但是没有简单的方法可以将SSH插入其中。

相关文章

网友评论

      本文标题:为容器构建自定义SSH服务

      本文链接:https://www.haomeiwen.com/subject/yrxxrktx.html