END GOAL:构建一个flask应用程序,该应用程序可以在远程计算机中进行SSH,然后将其折叠到远程设备(通过代理转发)并在该设备上获取配置文件的值。

问题:当我在本地计算机上运行flask应用程序时,我有一个脚本。它从远程设备获取并显示配置的值。但是,当我构建一个Docker容器来托管flask应用程序时,我只收到拒绝(publickey)的许可。 API的错误。我的Docker容器以一种我能够执行完全相同的SSH脚本以从容器终端获取配置的方式设置。但是,即使我curlAPI,它也会给我同样的权限拒绝错误。

这是我的小flaskAPI和脚本来获取配置的内容。

from flask import Flask, jsonify, request

app = Flask(__name__)

@app.get('/api/v1/remote/config')
def ssh_show_config():
    user = request.args.get('user')
    host = request.args.get('host')
    node = request.args.get('node')
    return jsonify(get_config_via_ssh(user, host, node))

def get_config_via_ssh(user, host, tunnel_ip):
    try:
        cmd2 = f"ssh -A -tt {user}@{host} -tt \"ssh root@{tunnel_ip} 'cat /storage/config'\""
        process = subprocess.getoutput(cmd2, encoding='utf-8')
        # ConfigCleaner is just a tool that cleans the data in the config
        cleaner = ConfigCleaner()
        return cleaner.clean(process)
    except Exception as e:
        print(e)
        print("[!] Cannot connect to the device SSH Server")
        exit()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

这是我的Dockerfile

FROM python:3.11-slim-bullseye

# update/install necessary alpine linux libraries
RUN apt-get -y update
RUN apt-get install -y openssh-server
RUN apt-get install -y openssh-client
RUN apt-get install -y curl
RUN apt-get install -y sudo

RUN adduser tester
RUN adduser tester sudo

RUN pip3 install poetry

# create project directory
WORKDIR /root/config-app
COPY . .

EXPOSE 5000
EXPOSE 22

# install required python libraries via poetry
RUN poetry config virtualenvs.create false
RUN poetry install

USER tester

# switch to app directory and run app
WORKDIR /root/config-app/src/config_fetcher

CMD poetry run flask --app app run --host 0.0.0.0 --port 5000

这是我的docker-compose

version: '3.8'
services:
  config-fetcher:
    image: config-app
    user: root
    volumes:
      - ~/.ssh/config:/root/.ssh/config:rw
      - ~/.ssh/id_ed25519:/root/.ssh/id_ed25519:rw
      - ~/.ssh/id_ed25519.pub:/root/.ssh/id_ed25519.pub:rw
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "5000:5000"
      - "22:22"

运行Docker组合后,我在容器的终端中执行以下命令以进行1。添加空白known_hosts文件和套接字目录和2。启动SSH代理,然后将其安装的密钥添加到容器中:

touch ~/.ssh/known_hosts && mkdir ~/.ssh/sockets && eval ssh-agent && ssh-add ~/.ssh/id_ed25519

毕竟,我能够执行

ssh -A -tt [email protected] -tt ssh [email protected] cat /storage/config

在容器的终端内部,它将成功返回配置文件的内容(就像我设计的flaskAPI一样)。但是,如果我为flask应用程序创建的API端点(从同一容器中),则会给我权限错误。如果我在不使用Docker的情况下在本地计算机上运行Blask应用程序,那么这都不是问题。

我真的在击败自己的头。

分析解答

这里有很多问题。

  1. 您使用ssh-agent是无效的。

    因为您仅作为交互式docker exec会话的一部分启动ssh-agent,所以flask应用程序产生的ssh进程将不可见。

  2. 我对您的ssh命令行感到怀疑,因为您有三个级别的报价。

    通过抛弃getoutput以支持check_output并将命令指定为列表,而不是字符串,我们可以简化事物。

    我们可以通过使用-J选项将SSH指定跳跃主机来进一步简化事物。

  3. Dockerfile中的EXPOSE什么都没做(这只是信息丰富)。


我们可以通过这样的编写来修复您的代码:

import subprocess
from flask import Flask, jsonify, request

app = Flask(__name__)


@app.get("/api/v1/remote/config")
def ssh_show_config():
    user = request.args.get("user")
    host = request.args.get("host")
    node = request.args.get("node")
    return jsonify(get_config_via_ssh(user, host, node))


def get_config_via_ssh(user, host, tunnel_ip):
    try:
        cmd = [
            "ssh",
            "-A",
            "-J", f"{user}@{host}",
            f"root@{tunnel_ip}",
            "cat /tmp/config",
        ]
        config = subprocess.check_output(cmd, encoding="utf-8", stderr=subprocess.PIPE)
        return {"config": config}
    exception Exception as e:
        return {"error": f"Cannot connect to the device SSH Server: {e}"}


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

我已经使用pipenv而不是poetry重写了Dockerfile(因为我不熟悉诗歌),并删除了一堆与此示例无关的无关代码。

我还修改了Dockerfile,以使用ssh-agent的控制来运行您的应用程序,并使用显式代理插座,以便我们可以从交互式会话中与同一代理进行交互:

FROM python:3.11-slim-bullseye

# update/install necessary alpine linux libraries
RUN apt-get -y update
RUN apt-get install -y openssh-client

RUN pip install pipenv

# create project directory
WORKDIR /app
COPY requirements.txt ./
RUN pipenv install -r requirements.txt
COPY . ./
RUN mkdir -m 700 -p /root/.ssh && cp ssh_config /root/.ssh/config && chmod 600 /root/.ssh/config

ENV SSH_AUTH_SOCK=/tmp/agent.sock

CMD ["ssh-agent", "-a", "/tmp/agent.sock", "pipenv", "run", "flask", \
    "--app", "app", "run", "--host", "0.0.0.0", "--port", "5000"]

最后,我已更新compose.yaml,将SSH键安装为Docker Secret

services:
  config-fetcher:
    image: flaskapp
    user: root
    build:
      context: .
    ports:
      - "5000:5000"
    secrets:
      - sshkey

secrets:
  sshkey:
    file: ~/.ssh/id_rsa

这意味着,当容器运行时,我的SSH键将以/run/secrets/sshkey的形式提供。


将所有这些都安装到位,如果我docker compose up容器,我可以启动一个交互式会话并加载密钥:

$ docker compose exec config-fetcher ssh-add /run/secrets/sshkey
Enter passphrase for /run/secrets/sshkey:
Identity added: /run/secrets/sshkey (/run/secrets/sshkey)

然后以下请求有效:

$ curl localhost:5000/api/v1/remote/config'?user=lars&host=172.20.0.1&node=127.0.0.1'
{"config":"this is a test\n"}