fix(deploy): 区分 Access 重部署与认证切换 / distinguish Access redeploys from auth switches
This commit is contained in:
1 parent
c6e5786bba
commit
ed523def41
5 files changed
+44
-7
No files matched your search
+2
-2
@@ -67,7 +67,7 @@ API Token 只存 GitHub Secret,不放普通变量、代码或命令行输入
|
||||
1. 校验本地配置,运行类型检查、测试、前端构建和 Wrangler dry-run。
|
||||
2. 自动发现唯一账户(显式账户 ID 优先),读取现有 Worker Secret **名称**,不尝试读取密钥明文。
|
||||
3. 有 `CUSTOM_DOMAIN` 时使用该入口;否则读取账户 `workers.dev` 子域,未注册时自动注册确定性名称。已有子域不改名,避免影响其他 Worker。
|
||||
4. 根据 `AUTH_PROVIDER` 仅准备所选认证:Cloudflare 核对实际 Access 应用、策略、Team Domain 与 AUD,GitHub 首次解析并固定管理员数字 ID,后续直接复用。
|
||||
4. 根据 `AUTH_PROVIDER` 仅准备所选认证:Cloudflare 普通重部署核对实际入口仍有 Access 网关,首次启用或从 GitHub 切回时核对应用、策略、Team Domain 与 AUD;GitHub 首次解析并固定管理员数字 ID,后续直接复用。
|
||||
5. GitHub 模式不调用 Zero Trust API;Cloudflare 模式不需要 GitHub OAuth 参数。既有 Access 应用不重写人工 IdP 配置。
|
||||
6. 按 ID 或名称复用 D1,不存在才创建;指定 ID 不存在时直接失败,不用新空库替代。
|
||||
7. 已有 `ENCRYPTION_KEY` 则保留;没有密钥且 D1 没有主机资料时,用安全随机数生成 32 字节密钥。
|
||||
@@ -113,7 +113,7 @@ EdgeSSH-Auto-Update: true
|
||||
- 新部署不需要 GitHub 写 Secrets 权限或额外 GitHub Token。生成的值不写入文件、命令行参数或 Actions artifact。
|
||||
- 重跑、推送新代码时保留原加密密钥。GitHub 中遗留的同名密钥不会替换 Worker 中的密钥。
|
||||
- **已有 D1 主机资料但缺少密钥时停止部署。** 必须恢复原密钥,不能生成新密钥假装修复。Cloudflare API 不提供 Secret 明文读回,自动生成的密钥也不会显示给用户;不要删除 Worker/Secret。需要独立灾备时,可在首次部署前自行生成并安全备份 32 字节 Base64 密钥,再保存为 `ENCRYPTION_KEY` GitHub Secret。
|
||||
- 旧部署无需重新输入邮箱,也不要求复制 Secret 回 GitHub;Cloudflare 模式每次从实际 Access 应用重新核对 Team Domain、AUD 与策略,不以 Secret 名称代替有效性检查,也不会改写人工维护的 IdP 或身份策略。
|
||||
- 旧部署无需重新输入邮箱,也不要求复制 Secret 回 GitHub;同为 Cloudflare 模式的普通重部署会实际探测入口仍受 Access 保护并保留现有 Secret,不要求新增 Zero Trust API 权限。首次启用或从 GitHub 切回时才从 Access 应用核对 Team Domain、AUD 与策略;任何路径都不会仅凭 Secret 名称判定有效,也不会改写人工维护的 IdP 或身份策略。
|
||||
- 若需要自动创建/管理 Access,或更换 hostname,请提供 `ADMIN_EMAIL`。自动管理使用账户级 Access API;原有 Zone 级应用请先核对,不要在同一 hostname 叠加应用。
|
||||
- 切换认证方式不改变 `ENCRYPTION_KEY`、D1 和管理员资料所有者。不要通过更换密钥来切换登录方式。
|
||||
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ https://developers.cloudflare.com/cloudflare-one/access-controls/applications/ht
|
||||
| `ACCESS_AUD` | `012345…`(常见外观) | 前面取得的应用受众 (AUD) 标签(Application Audience (AUD) Tag),必须原样复制实际值 |
|
||||
| `ENCRYPTION_KEY` | `AbCd…=`(44 字符 Base64) | 32 字节安全随机数的标准 Base64,只在首次部署时生成,后续始终复用;不要使用示例文本 |
|
||||
|
||||
`Deploy` 通过标准输入写入机密,后续保留 Worker 中的加密密钥。它们不会写入 `wrangler.toml`、临时文件、普通变量或提交记录。普通重部署会从实际 Access 应用重新取得 Team Domain 与 AUD,并核对现有策略;不会仅凭旧 Secret 名称假定入口有效,也不会覆盖人工维护的 IdP 或明确身份策略。
|
||||
`Deploy` 通过标准输入写入机密,后续保留 Worker 中的加密密钥。它们不会写入 `wrangler.toml`、临时文件、普通变量或提交记录。同为 Cloudflare 模式的普通重部署会实际确认入口仍受 Access 保护并保留已有 Team Domain/AUD;首次启用或从 GitHub 切回时才通过 Access API 核对应用与策略。部署不会仅凭旧 Secret 名称假定入口有效,也不会覆盖人工维护的 IdP 或明确身份策略。
|
||||
|
||||
`ENCRYPTION_KEY` 的生成要求和保管注意事项见[部署指南](../DEPLOYMENT.md#worker-runtime-secrets)。生产部署不需要在本地执行 Wrangler 机密(Secret)或 Worker 部署命令。
|
||||
|
||||
|
||||
+6
-1
@@ -45,6 +45,11 @@ async function main(): Promise<void> {
|
||||
const hostname = settings.customDomain || await resolveWorkerHostname(api, settings);
|
||||
const database = await ensureDatabase(settings);
|
||||
const workspace = await readWorkspaceState(api, settings, database);
|
||||
const deployedProvider = workspace.authProvider
|
||||
?? await readWorkerVariable(api, settings, 'AUTH_PROVIDER');
|
||||
if (deployedProvider !== undefined && deployedProvider !== 'cloudflare' && deployedProvider !== 'github') {
|
||||
throw new Error('已部署的认证方式无效,停止自动覆盖。');
|
||||
}
|
||||
const deployedGitHubId = workspace.githubAdminId
|
||||
?? (settings.authProvider === 'github' ? await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID') : undefined);
|
||||
if (deployedGitHubId && !/^[1-9]\d*$/.test(deployedGitHubId)) {
|
||||
@@ -53,7 +58,7 @@ async function main(): Promise<void> {
|
||||
const authentication = await prepareAuthentication(api, settings, hostname, {
|
||||
existingSecrets,
|
||||
fixedGithubAdminId: deployedGitHubId,
|
||||
previousProvider: workspace.authProvider,
|
||||
previousProvider: deployedProvider,
|
||||
});
|
||||
const secrets = { ...authentication.secrets, ...await prepareEncryptionSecret(api, settings, database, existingSecrets) };
|
||||
maskSecrets(secrets);
|
||||
|
||||
@@ -21,7 +21,25 @@ export async function prepareAuthentication(
|
||||
): Promise<{ secrets: Partial<RuntimeSecrets>; githubAdminId?: string }> {
|
||||
const fetcher = options.fetcher ?? fetch;
|
||||
if (settings.authProvider === 'cloudflare') {
|
||||
// 每次从对应 Access 应用刷新 Team Domain/AUD;Secret 名称本身不能证明入口仍然有效。
|
||||
const canRetain = options.previousProvider === 'cloudflare'
|
||||
&& requiredAuthSecrets('cloudflare').every((name) => options.existingSecrets?.has(name));
|
||||
if (canRetain) {
|
||||
try {
|
||||
const response = await fetcher(`https://${hostname}/api/auth/me`, {
|
||||
redirect: 'manual', signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
const location = response.headers.get('Location');
|
||||
const target = location ? new URL(location, `https://${hostname}`) : null;
|
||||
if (target && (target.hostname.endsWith('.cloudflareaccess.com')
|
||||
|| target.pathname.startsWith('/cdn-cgi/access/'))) {
|
||||
console.log('已确认当前入口仍由 Cloudflare Access 保护,保留现有认证 Secret。');
|
||||
return { secrets: {} };
|
||||
}
|
||||
} catch {
|
||||
throw new Error('无法确认当前入口的 Cloudflare Access 登录是否可用,请检查域名后重试。');
|
||||
}
|
||||
}
|
||||
// 首次启用、切回 Access 或现有配置不完整时,必须从对应应用刷新配置。
|
||||
return { secrets: await ensureAccess(api, settings, hostname) };
|
||||
}
|
||||
// GitHub 新部署完全不调用 Zero Trust API。旧域名若仍在 Access 后面,明确停止,
|
||||
|
||||
@@ -100,7 +100,19 @@ test('a mode switch stops rather than leaving GitHub behind an existing Access l
|
||||
}), /无法确认入口/);
|
||||
});
|
||||
|
||||
test('Cloudflare redeployment validates the real Access app instead of trusting secret names', async () => {
|
||||
test('ordinary Cloudflare redeployment confirms the live Access gateway without requiring Zero Trust API', async () => {
|
||||
const cf = readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token' });
|
||||
const api = new CloudflareApi('token', async () => { throw Error('must not call Zero Trust'); });
|
||||
assert.deepEqual(await prepareAuthentication(api, cf, 'ssh.example.com', {
|
||||
previousProvider: 'cloudflare',
|
||||
existingSecrets: new Set(['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']),
|
||||
fetcher: async () => new Response(null, {
|
||||
status: 302, headers: { Location: 'https://team.cloudflareaccess.com/cdn-cgi/access/login' },
|
||||
}),
|
||||
}), { secrets: {} });
|
||||
});
|
||||
|
||||
test('switching to Cloudflare validates the real Access app instead of trusting old secret names', async () => {
|
||||
const cf = readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token' });
|
||||
const paths: string[] = [];
|
||||
const api = new CloudflareApi('token', async (url) => {
|
||||
@@ -113,7 +125,9 @@ test('Cloudflare redeployment validates the real Access app instead of trusting
|
||||
}]);
|
||||
throw Error(`unexpected ${path}`);
|
||||
});
|
||||
assert.deepEqual(await prepareAuthentication(api, cf, 'ssh.example.com'), {
|
||||
assert.deepEqual(await prepareAuthentication(api, cf, 'ssh.example.com', {
|
||||
previousProvider: 'github', existingSecrets: new Set(['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']),
|
||||
}), {
|
||||
secrets: { ACCESS_TEAM_DOMAIN: 'team.cloudflareaccess.com', ACCESS_AUD: 'current-aud' },
|
||||
});
|
||||
assert.ok(paths.some((path) => path.endsWith('/organizations')));
|
||||
|
||||
Reference in new issue
Block a user