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应用程序,那么这都不是问题。
我真的在击败自己的头。
这里有很多问题。
您使用
ssh-agent
是无效的。因为您仅作为交互式
docker exec
会话的一部分启动ssh-agent,所以flask应用程序产生的ssh
进程将不可见。我对您的
ssh
命令行感到怀疑,因为您有三个级别的报价。通过抛弃
getoutput
以支持check_output
并将命令指定为列表,而不是字符串,我们可以简化事物。我们可以通过使用
-J
选项将SSH指定跳跃主机来进一步简化事物。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"}