容器技术虽然提供了环境隔离,但仍存在潜在的安全风险。本笔记将探讨容器安全的基本原则,分析OpenHands中的安全考量,并实现一套容器安全加固方案,确保在保持功能性的同时提升系统安全性。
Docker安全架构基于以下Linux内核安全机制:
Namespace隔离:
Cgroups限制:
能力控制(Capabilities):
Seccomp-BPF过滤:
AppArmor/SELinux:
镜像安全:
运行时安全:
网络安全:
存储安全:
监控与审计:
从README_CN.md中,我们可以看到OpenHands项目特别强调了容器安全加固:
双层容器结构:
Docker Socket使用:
强化安装指南:
持久化数据安全:
~/.openhands
目录权限管理从OpenHands的Docker命令中,我们可以识别以下潜在风险点:
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
Docker Socket挂载:
端口暴露:
-p 3000:3000
可能将服务暴露给所有网络接口主机网络访问:
--add-host host.docker.internal:host-gateway
允许访问宿主机卷挂载:
我们将从以下几个方面进行安全加固:
#!/bin/bash
# 创建专用网络(如果不存在)
if ! docker network inspect openhands-net &>/dev/null; then
docker network create openhands-net
fi
# 创建专用用户和组
OPENHANDS_UID=$(id -u)
OPENHANDS_GID=$(id -g)
# 创建并设置数据目录权限
mkdir -p ~/.openhands
chmod 700 ~/.openhands
# 运行加固的OpenHands容器
docker run \
--rm \
--pull=always \
--name openhands-app \
# 安全限制
--security-opt=no-new-privileges \
--cap-drop=ALL \
--cap-add=CHOWN \
--cap-add=SETGID \
--cap-add=SETUID \
--cap-add=DAC_OVERRIDE \
# 资源限制
--cpus=2 \
--memory=2g \
--pids-limit=100 \
# 网络安全配置
--network openhands-net \
--publish 127.0.0.1:3000:3000 \
# 环境变量
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-e OPENHANDS_UID=$OPENHANDS_UID \
-e OPENHANDS_GID=$OPENHANDS_GID \
# Docker Socket安全挂载
-v /var/run/docker.sock:/var/run/docker.sock:ro \
# 数据卷挂载
-v ~/.openhands:/.openhands:rw \
# 容器镜像
docker.all-hands.dev/all-hands-ai/openhands:0.47
使用Docker Socket代理可以限制对Docker API的访问权限:
# Dockerfile.docker-proxy
FROM alpine:3.17
RUN apk add --no-cache socat
# 创建非root用户
RUN adduser -D -u 1000 dockproxy
USER dockproxy
# 启动socat代理,仅允许特定API访问
CMD ["socat", \
"TCP-LISTEN:2375,fork,reuseaddr", \
"UNIX-CONNECT:/var/run/docker.sock"]
使用此代理的启动脚本:
#!/bin/bash
# 启动Docker Socket代理
docker run -d \
--name openhands-docker-proxy \
--restart=unless-stopped \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--network openhands-net \
openhands-docker-proxy
# 然后使用代理启动OpenHands
docker run \
--rm \
--pull=always \
--name openhands-app \
# 安全限制同上
# ...
# 使用代理替代直接挂载Docker Socket
-e DOCKER_HOST=tcp://openhands-docker-proxy:2375 \
# 其他配置同上
# ...
docker.all-hands.dev/all-hands-ai/openhands:0.47
对于OpenHands创建的沙箱容器,实施以下安全加固措施:
# 示例沙箱容器启动命令
docker run \
-d \
--name "sandbox-${SESSION_ID}" \
# 严格的安全选项
--security-opt=no-new-privileges \
--security-opt seccomp=sandbox-seccomp.json \
--cap-drop=ALL \
# 最小必要权限
--cap-add=CHOWN \
--cap-add=SETUID \
--cap-add=SETGID \
# 严格的资源限制
--cpus=0.5 \
--memory=512m \
--memory-swap=512m \
--pids-limit=50 \
# 禁用主机网络访问
--network none \
# 设置工作目录为只读,仅允许写入特定目录
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
# 工作目录挂载
-v "workspace-${SESSION_ID}:/workspace:rw" \
# 沙箱镜像
docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
# 默认命令
"/bin/sh"
创建限制性更强的seccomp配置文件,限制可用的系统调用:
// sandbox-seccomp.json
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept", "accept4", "access", "arch_prctl", "bind",
"brk", "capget", "capset", "chdir", "chmod",
"chown", "chroot", "clock_getres", "clock_gettime", "clock_nanosleep",
"close", "connect", "copy_file_range", "creat", "dup",
"dup2", "dup3", "epoll_create", "epoll_create1", "epoll_ctl",
"epoll_pwait", "epoll_wait", "eventfd", "eventfd2", "execve",
"execveat", "exit", "exit_group", "faccessat", "fadvise64",
"fallocate", "fanotify_mark", "fchdir", "fchmod", "fchmodat",
"fchown", "fchownat", "fcntl", "fdatasync", "fgetxattr",
"flistxattr", "flock", "fork", "fremovexattr", "fsetxattr",
"fstat", "fstatfs", "fsync", "ftruncate", "futex",
"getcwd", "getdents", "getdents64", "getegid", "geteuid",
"getgid", "getgroups", "getitimer", "getpeername", "getpgid",
"getpgrp", "getpid", "getppid", "getpriority", "getrandom",
"getresgid", "getresuid", "getrlimit", "getrusage", "getsid",
"getsockname", "getsockopt", "gettid", "gettimeofday", "getuid",
"getxattr", "inotify_add_watch", "inotify_init", "inotify_init1",
"inotify_rm_watch", "io_cancel", "io_destroy", "io_getevents",
"io_setup", "io_submit", "ioctl", "kill", "lchown",
"lgetxattr", "link", "linkat", "listen", "listxattr",
"llistxattr", "lremovexattr", "lseek", "lsetxattr", "lstat",
"madvise", "memfd_create", "mincore", "mkdir", "mkdirat",
"mknod", "mknodat", "mlock", "mmap", "mount",
"mprotect", "mq_getsetattr", "mq_notify", "mq_open", "mq_timedreceive",
"mq_timedsend", "mq_unlink", "mremap", "msgctl", "msgget",
"msgrcv", "msgsnd", "msync", "munlock", "munmap",
"nanosleep", "newfstatat", "open", "openat", "pause",
"pipe", "pipe2", "poll", "ppoll", "prctl",
"pread64", "preadv", "preadv2", "prlimit64", "pselect6",
"pwrite64", "pwritev", "pwritev2", "read", "readahead",
"readlink", "readlinkat", "readv", "recv", "recvfrom",
"recvmmsg", "recvmsg", "rename", "renameat", "renameat2",
"restart_syscall", "rmdir", "rt_sigaction", "rt_sigpending",
"rt_sigprocmask", "rt_sigqueueinfo", "rt_sigreturn", "rt_sigsuspend",
"rt_sigtimedwait", "rt_tgsigqueueinfo", "sched_get_priority_max",
"sched_get_priority_min", "sched_getaffinity", "sched_getattr",
"sched_getparam", "sched_getscheduler", "sched_rr_get_interval",
"sched_setaffinity", "sched_setattr", "sched_setparam",
"sched_setscheduler", "sched_yield", "seccomp", "select",
"semctl", "semget", "semop", "semtimedop", "send",
"sendfile", "sendmmsg", "sendmsg", "sendto", "set_robust_list",
"set_tid_address", "setgid", "setgroups", "setitimer", "setpgid",
"setpriority", "setregid", "setresgid", "setresuid", "setreuid",
"setrlimit", "setsid", "setsockopt", "setuid", "setxattr",
"shmat", "shmctl", "shmdt", "shmget", "shutdown",
"sigaltstack", "socket", "socketpair", "stat", "statfs",
"statx", "symlink", "symlinkat", "sync", "sync_file_range",
"syncfs", "sysinfo", "tee", "tgkill", "time",
"timer_create", "timer_delete", "timer_getoverrun", "timer_gettime",
"timer_settime", "timerfd_create", "timerfd_gettime", "timerfd_settime",
"times", "tkill", "truncate", "umask", "uname",
"unlink", "unlinkat", "utime", "utimensat", "utimes",
"vfork", "wait4", "waitid", "waitpid", "write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
创建AppArmor配置文件限制沙箱容器权限:
# /etc/apparmor.d/openhands-sandbox
#include
profile openhands-sandbox {
#include
#include
#include
# 容器文件系统访问权限
/ r,
/** r,
/workspace/** rw,
/tmp/** rw,
# 允许执行脚本和命令
/bin/* ix,
/usr/bin/* ix,
# 网络访问限制
network tcp,
network udp,
# 能力限制
capability chown,
capability setuid,
capability setgid,
capability dac_override,
# 拒绝其他危险操作
deny capability sys_admin,
deny capability sys_ptrace,
deny capability sys_boot,
deny capability sys_module,
# 拒绝挂载和文件系统操作
deny mount,
deny umount,
deny /proc/** wl,
deny /sys/** wl,
deny /dev/** wl,
# 拒绝写入敏感文件
deny /etc/** w,
deny /var/log/** w,
}
使用AppArmor配置启动沙箱:
# 启用AppArmor配置
sudo apparmor_parser -r -W /etc/apparmor.d/openhands-sandbox
# 启动带有AppArmor的沙箱
docker run \
# 其他选项同上
# ...
--security-opt apparmor=openhands-sandbox \
# ...
docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
创建容器安全检查脚本:
#!/bin/bash
# 检查OpenHands容器安全配置
check_openhands_security() {
local CONTAINER_NAME="openhands-app"
echo "检查 $CONTAINER_NAME 容器安全配置..."
# 检查是否运行
if ! docker ps | grep -q "$CONTAINER_NAME"; then
echo "❌ 容器 $CONTAINER_NAME 未运行"
return 1
fi
# 获取容器信息
local CONTAINER_INFO=$(docker inspect "$CONTAINER_NAME")
# 检查特权模式
if echo "$CONTAINER_INFO" | grep -q '"Privileged": true'; then
echo "❌ 容器运行在特权模式,这是不安全的"
else
echo "✅ 容器未使用特权模式"
fi
# 检查能力设置
local CAPS=$(echo "$CONTAINER_INFO" | grep -o '"CapAdd": \[[^]]*\]')
echo " 容器能力设置: $CAPS"
# 检查网络设置
local NETWORK_MODE=$(echo "$CONTAINER_INFO" | grep -o '"NetworkMode": "[^"]*"')
echo " 容器网络模式: $NETWORK_MODE"
# 检查端口绑定
local PORT_BINDINGS=$(echo "$CONTAINER_INFO" | grep -A10 '"PortBindings":' | grep -B10 '}')
if echo "$PORT_BINDINGS" | grep -q '0.0.0.0'; then
echo "⚠️ 容器端口绑定到所有网络接口,可能暴露给公网"
else
echo "✅ 容器端口绑定到本地接口"
fi
# 检查挂载点
local MOUNTS=$(echo "$CONTAINER_INFO" | grep -A50 '"Mounts":' | grep -B50 '\]')
echo " 容器挂载点:"
echo "$MOUNTS" | grep "Source\|Destination\|Mode" | sed 's/^[ \t]*/ /'
# 检查Docker Socket挂载
if echo "$MOUNTS" | grep -q "docker.sock"; then
if echo "$MOUNTS" | grep -q '"RW": true' && echo "$MOUNTS" | grep -q "docker.sock"; then
echo "❌ Docker Socket以读写模式挂载,存在安全风险"
else
echo "⚠️ Docker Socket以只读模式挂载,风险较低但仍然存在"
fi
else
echo "✅ 未直接挂载Docker Socket"
fi
# 检查资源限制
local CPU_LIMIT=$(echo "$CONTAINER_INFO" | grep -o '"NanoCpus": [0-9]*')
local MEM_LIMIT=$(echo "$CONTAINER_INFO" | grep -o '"Memory": [0-9]*')
local PIDS_LIMIT=$(echo "$CONTAINER_INFO" | grep -o '"PidsLimit": [0-9]*')
echo " 资源限制:"
echo " $CPU_LIMIT"
echo " $MEM_LIMIT"
echo " $PIDS_LIMIT"
# 检查安全选项
local SECURITY_OPTS=$(echo "$CONTAINER_INFO" | grep -o '"SecurityOpt": \[[^]]*\]')
echo " 安全选项: $SECURITY_OPTS"
echo "检查完成"
}
check_openhands_security
定期执行安全审计的脚本:
#!/bin/bash
# OpenHands运行时安全审计
echo "=== OpenHands安全审计 $(date) ==="
# 1. 检查运行的容器
echo "--- 检查运行的容器 ---"
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"
# 2. 检查Docker Socket权限
echo -e "\n--- 检查Docker Socket权限 ---"
ls -la /var/run/docker.sock
echo "所属组: $(stat -c '%G' /var/run/docker.sock)"
# 3. 检查OpenHands数据目录权限
echo -e "\n--- 检查OpenHands数据目录权限 ---"
ls -la ~/.openhands
# 4. 检查沙箱容器安全状态
echo -e "\n--- 检查沙箱容器 ---"
docker ps --filter "name=sandbox-" --format "table {{.ID}}\t{{.Names}}\t{{.Status}}"
for container in $(docker ps --filter "name=sandbox-" --format "{{.Names}}"); do
echo -e "\n容器: $container 详细信息"
docker inspect --format '{{json .HostConfig.SecurityOpt}}' "$container"
docker inspect --format '{{json .HostConfig.CapDrop}}' "$container"
docker inspect --format '{{json .HostConfig.CapAdd}}' "$container"
docker inspect --format '{{json .HostConfig.Resources}}' "$container"
done
# 5. 检查网络状态
echo -e "\n--- 检查网络状态 ---"
docker network ls
docker network inspect openhands-net
# 6. 检查日志中的安全事件
echo -e "\n--- 检查安全日志 ---"
docker logs --tail 50 openhands-app | grep -i 'security\|warn\|error\|denied'
echo -e "\n=== 审计完成 ==="
最小权限原则:
--security-opt=no-new-privileges
防止特权提升多层防御策略:
网络安全加固:
Docker Socket保护:
数据保护:
以下是OpenHands部署前的安全检查清单: