主机初始化中的 init-state.sh 已经创建两个凭据文件:
secrets/root_user:默认管理员名为file-code-root。secrets/root_password:使用openssl rand -hex 32生成随机密码。
重复执行初始化不会覆盖已有凭据。先检查文件和数据目录的归属、权限:
cd /app/file-code-stack
sudo stat -c '%u:%g %a %n' \
secrets/root_user \
secrets/root_password \
/data/otterio两个凭据文件都应为 20002:20002、0400,数据目录应为 20002:20002、0750。
默认管理员名可以直接使用。如果希望更换,在首次启动前执行下面的命令,将示例名称替换为自己的名称:
(
set -euo pipefail
printf '%s\n' 'your-admin-name' |
sudo tee /app/file-code-stack/secrets/root_user >/dev/null
sudo chown 20002:20002 /app/file-code-stack/secrets/root_user
sudo chmod 0400 /app/file-code-stack/secrets/root_user
)在安全终端查看凭据,并保存到密码管理器:
sudo cat /app/file-code-stack/secrets/root_user
sudo cat /app/file-code-stack/secrets/root_password秘密文件不提交 Git,也不复制到聊天、日志或工单。服务启动后遇到问题,先检查日志、配置和文件权限,不通过重新生成密码尝试修复。需要修改管理员凭据时,单独安排变更,并更新使用该凭据的客户端。
仓库通过 .env 中的 OTTERIO_IMAGE 固定镜像,通过 /data/otterio 保存数据,通过 Compose secrets 挂载凭据,无需在 shell 中 export 管理员密码。
OtterIO 使用镜像自带的入口脚本加载用户名和密码。下面是仓库中已有的配置片段,用于说明读取方式,不需要另外创建一份 Compose 文件:
command: [server, --address, ':9000', --console-address, ':9001', /data]
environment:
HOME: /tmp
OTTERIO_BROWSER: "off"
OTTERIO_REGION_NAME: us-east-1
OTTERIO_ROOT_USER_FILE: /run/secrets/root_user
OTTERIO_ROOT_PASSWORD_FILE: /run/secrets/root_password
OTTERIO_ACCESS_KEY_FILE: ''
OTTERIO_SECRET_KEY_FILE: ''
secrets: [root_user, root_password]两个 _FILE 变量指向容器内的绝对路径,Compose 将宿主机上的凭据文件挂载到对应位置。保留镜像原有的 entrypoint,不绕过它的凭据检查。
这个版本要求显式配置非默认凭据。这里统一使用文件方式,不再为同一个设置提供非空环境变量,也不混用新旧两组凭据变量。文件必须可读、非空且为普通文件;镜像不会自动调整数据目录归属,启动前需要核对权限,具体要求见版本发布说明。
先在自己的电脑上建立 SSH 隧道。服务器地址和密钥路径按实际情况填写:
SERVER_IP=203.0.113.10
ADMIN_KEY="$HOME/.ssh/file-code-stack_ed25519"
ssh -N \
-o IdentitiesOnly=yes \
-o ExitOnForwardFailure=yes \
-i "$ADMIN_KEY" \
-L 127.0.0.1:19001:127.0.0.1:19001 \
-L 127.0.0.1:19000:127.0.0.1:19000 \
"deploy@$SERVER_IP"命令运行期间保留这个终端。19001 用于 OC 连接管理 API,19000 用于连接 S3 API;本地监听地址明确绑定为 127.0.0.1,不向管理电脑所在网络开放。
按 OC 管理界面在管理电脑构建固定版本的 OC,设置 S3 与独立管理地址,启动 oc-console --alias store --allow-writes,浏览器打开它打印的本机 URL 并输入 login code。19001 只提供管理 API,OtterIO 自带界面已关闭。
在控制台完成下面几步:
- 创建
images桶,保持默认私有状态。 - 创建图片上传专用账号,将图片上传策略 的完整 JSON 保存为它的权限策略;上传客户端只使用这个账号的访问键。
- 确认桶内只有允许公开的图片,再将图片公开策略 的完整 JSON 保存为该桶的匿名读取策略。
- 使用专用账号上传一个测试对象,再分别验证匿名读取成功、匿名写入和列桶失败,以及该账号不能访问其他桶。
策略文件随前面的配置目录一并下载。操作时区分用户或账号的权限策略与桶的匿名策略,不把两个 JSON 填到同一处。控制台的菜单和管理能力随版本变化;如果所选版本缺少创建账号或设置策略的入口,先补齐经过该版本验证的管理工具,再继续开放服务。
账号策略和桶策略用途不同:前者限制上传账号可以执行的操作,后者决定匿名访问是否允许。其他桶按用途单独配置,不一起公开。
仓库的公开策略只允许 GetObject,不允许匿名列桶、上传或删除。对象也可以通过 S3 域名读取,但这个入口仍受来源 IP 限制。
如果控制台不能导入所需策略,先确认所选管理工具与当前 OtterIO 版本兼容。S3 兼容并不代表管理 API 也兼容,不直接套用其他项目的账号管理命令。
程序连接时,endpoint 设置为 https://s3.实际域名,region 使用 us-east-1,启用 path-style。每个应用使用自己的账号,不把管理员密钥交给插件、网页或其他应用。
图片上传策略只授权 images 桶内的 GetObject 和 PutObject,不开放删除或列桶。需要分片上传时,再根据客户端调用的 API 补充权限,并在测试桶验证。
图片入口只允许 /images/ 下的 GET、HEAD。例如,桶中的对象 key 为 demo/example.png,公开地址就是:
https://图片域名/images/demo/example.png
先使用便于检查的文件名和目录结构完成上传、读取验证,再测试特殊字符、URL 编码及条件请求。不要通过改写上传请求路径来调整公开链接,以免影响 S3 签名。
下载文件使用 downloads 桶,将下载公开策略 应用为桶策略,上传账号使用下载上传策略。
下载上传账号与图片上传账号分开,只授权各自桶内的对象读写,不开放删除或列桶。需要分片上传时,同样先在测试桶验证所需权限。
例如,一个发布包的公开地址是:
https://files.实际域名/downloads/releases/demo/v1.0.0/demo-linux-amd64.tar.gz
下载路由只允许 /downloads/ 下的 GET、HEAD,上传仍通过 S3 域名并使用签名认证。对象是否允许匿名读取,最终还取决于桶策略。
发布包按版本目录或内容哈希保存,不覆盖已经发布的对象。上传策略中的 PutObject 本身允许覆盖已有 key,因此这里还需要由发布流程保证文件名不重复。
每次发布同时提供 SHA-256 校验文件,重要软件再提供签名。校验和用于检查内容是否一致,不能单独证明发布者身份。私有文件不要临时放进公开桶。
上传时设置正确的 Content-Type。需要下载保存的压缩包和二进制文件,可以设置 Content-Disposition: attachment;图片保持正常展示,不给所有对象统一追加下载头。元数据通过 SDK 或兼容工具设置,上传后检查实际响应。
下载入口需要验证 GET、HEAD、Range 和 ETag 条件请求,并确认写请求被拒绝。下面使用一个大于 1,024 字节的测试文件,检查响应头并读取前 1,024 字节:
FILE_URL='https://files.实际域名/downloads/releases/demo/v1.0.0/demo-linux-amd64.tar.gz'
curl -fsSI "$FILE_URL"
curl -fsS \
-D /tmp/file-range.headers \
-H 'Range: bytes=0-1023' \
"$FILE_URL" \
-o /tmp/file-range.bin
cat /tmp/file-range.headers
wc -c /tmp/file-range.binRange 请求应返回 206 Partial Content,Content-Range 中的范围应为 bytes 0-1023/文件总长度,保存的内容应为 1,024 字节。只看到 curl 成功退出不够,还要核对这些结果。
再复制响应中的完整 ETag 值,包括双引号,测试条件请求:
ETAG='"替换为实际ETag值"'
curl -sS -D - -o /dev/null \
-H "If-None-Match: $ETAG" \
"$FILE_URL"对象未变化时应返回 304 Not Modified。不要把 ETag 一律当成文件的 MD5 校验值,文件完整性仍使用发布时提供的 SHA-256 校验文件检查。
接入 CDN 后,再通过 CDN 域名重复这些测试,确认范围请求、条件请求和响应元数据仍然符合预期。
程序数据使用独立桶或明确的前缀,并创建独立账号。默认不设置匿名访问策略,读、写、删除权限按业务需要分别授权,不共用图片上传账号。
私有文件可以通过签名 S3 请求下载,也可以由应用检查用户权限后生成短期预签名链接。
不过,预签名链接不会绕过 Traefik 的来源 IP 限制。当前 S3 入口只允许配置中的客户端来源,因此普通读者不能直接使用这个入口的预签名链接。如果需要向读者提供私有下载,另行设计下载入口,并检查授权和缓存行为,避免私有响应被公开缓存。
桶策略可通过标准 S3 PutBucketPolicy 应用,不要求控制台具有导入菜单。下面是已安装 AWS CLI 时的可选操作,在部署服务器执行;images 桶应已创建,桶内内容已确认可以公开。
应用操作会替换现有 Bucket Policy,有需要保留的规则时先导出并合并。账号 IAM 策略仍使用与所选 OtterIO 版本兼容的管理接口,不能用 Bucket Policy 代替上传账号的权限配置。
sudo bash <<'S3POLICY'
set -euo pipefail
cd /app/file-code-stack
command -v aws >/dev/null
export AWS_ACCESS_KEY_ID="$(cat secrets/root_user)"
export AWS_SECRET_ACCESS_KEY="$(cat secrets/root_password)"
export AWS_DEFAULT_REGION=us-east-1
export AWS_EC2_METADATA_DISABLED=true
unset AWS_SESSION_TOKEN AWS_PROFILE AWS_DEFAULT_PROFILE
aws --endpoint-url http://127.0.0.1:19000 s3api put-bucket-policy \
--bucket images --policy file://policies/public-images.json
aws --endpoint-url http://127.0.0.1:19000 s3api get-bucket-policy \
--bucket images
S3POLICY凭据只在这个子进程中加载,不写入命令参数或 Git;不要启用 shell tracing。下载桶使用 downloads 与 policies/public-downloads.json。完成后不带凭据请求已存在的测试对象,应返回 200;匿名列桶应拒绝。实际工具兼容性和业务请求仍需验证。
默认 URL 保留 /images/。需要 https://图片域名/2023/05/20/example.jpg 时,可以使用可选动态路由示例,内部给日期路径加上 /images;浏览器地址保持原样。
先复制示例到自己的受控配置位置,将 img.example.com 改为 .env 的实际图片域名,保留只允许 GET/HEAD 的条件;再安装到 file provider 监听目录。下面在服务器执行,要求已有配置生成结果:
(
set -euo pipefail
cd /app/file-code-stack
sudo install -d -o root -g root -m 0750 traefik/custom
sudo test ! -e traefik/custom/image-short-url.yaml
sudo install -o root -g root -m 0644 \
examples/traefik/image-short-url.yaml.example \
traefik/custom/image-short-url.yaml
sudo env EDITOR=vim VISUAL=vim vim -n -i NONE \
-c 'set nobackup nowritebackup' traefik/custom/image-short-url.yaml
sudo install -o root -g root -m 0644 \
traefik/custom/image-short-url.yaml \
traefik/generated/dynamic/image-short-url.yaml
sudo docker compose logs --since 5m traefik
)首次安装才执行复制示例的命令;后续编辑 traefik/custom/image-short-url.yaml 并重新安装,不覆盖已定制的内容。额外动态文件不由生成器或 preflight 验证;部署后检查日志和真实请求。受控源文件 traefik/custom/image-short-url.yaml 不提交 Git,会随 /app/file-code-stack 一起备份。
对象仍保存在 images 桶,对象键不带 images/。签名上传走原始 S3 路径;不要改写上传请求。CDN 若已添加桶前缀,只在一处处理,避免双重前缀。此示例仅匹配日期目录,原有 /images/ 地址仍可访问。