mirror of
https://github.com/withastro/astro.git
synced 2025-03-31 23:31:30 -05:00
Allow passing into the crypto key via ASTRO_KEY (#11879)
* Allow passing into the crypto key via ASTRO_KEY * Add a changeset * Add test * Use the node package * omg * Create a new create-key command * linting * lint again * Update the changeset
This commit is contained in:
parent
e55c668b25
commit
bd1d4aaf82
14 changed files with 189 additions and 2 deletions
23
.changeset/four-tips-accept.md
Normal file
23
.changeset/four-tips-accept.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Allow passing a cryptography key via ASTRO_KEY
|
||||
|
||||
For Server islands Astro creates a cryptography key in order to hash props for the islands, preventing accidental leakage of secrets.
|
||||
|
||||
If you deploy to an environment with rolling updates then there could be multiple instances of your app with different keys, causing potential key mismatches.
|
||||
|
||||
To fix this you can now pass the `ASTRO_KEY` environment variable to your build in order to reuse the same key.
|
||||
|
||||
To generate a key use:
|
||||
|
||||
```
|
||||
astro create-key
|
||||
```
|
||||
|
||||
This will print out an environment variable to set like:
|
||||
|
||||
```
|
||||
ASTRO_KEY=PIAuyPNn2aKU/bviapEuc/nVzdzZPizKNo3OqF/5PmQ=
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
adapter: node({ mode: 'standalone' }),
|
||||
integrations: [],
|
||||
experimental: {
|
||||
serverIslands: true,
|
||||
}
|
||||
});
|
12
packages/astro/e2e/fixtures/server-islands-key/package.json
Normal file
12
packages/astro/e2e/fixtures/server-islands-key/package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@e2e/server-islands-key",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/node": "^8.3.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
const { secret } = Astro.props;
|
||||
---
|
||||
<h2 id="island">I am an island</h2>
|
||||
<slot />
|
||||
<h3 id="secret">{secret}</h3>
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import Island from '../components/Island.astro';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<!-- Head Stuff -->
|
||||
</head>
|
||||
<body>
|
||||
<Island server:defer secret="test">
|
||||
<h3 id="children">children</h3>
|
||||
</Island>
|
||||
</body>
|
||||
</html>
|
33
packages/astro/e2e/server-islands-key.test.js
Normal file
33
packages/astro/e2e/server-islands-key.test.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { testFactory } from './test-utils.js';
|
||||
|
||||
const test = testFactory(import.meta.url, { root: './fixtures/server-islands-key/' });
|
||||
|
||||
test.describe('Server islands - Key reuse', () => {
|
||||
test.describe('Production', () => {
|
||||
let previewServer;
|
||||
|
||||
test.beforeAll(async ({ astro }) => {
|
||||
// Playwright's Node version doesn't have these functions, so stub them.
|
||||
process.stdout.clearLine = () => {};
|
||||
process.stdout.cursorTo = () => {};
|
||||
process.env.ASTRO_KEY = 'eKBaVEuI7YjfanEXHuJe/pwZKKt3LkAHeMxvTU7aR0M=';
|
||||
await astro.build();
|
||||
previewServer = await astro.preview();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await previewServer.stop();
|
||||
delete process.env.ASTRO_KEY;
|
||||
});
|
||||
|
||||
test('Components render properly', async ({ page, astro }) => {
|
||||
await page.goto(astro.resolveUrl('/'));
|
||||
|
||||
let el = page.locator('#island');
|
||||
|
||||
await expect(el, 'element rendered').toBeVisible();
|
||||
await expect(el, 'should have content').toHaveText('I am an island');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,6 +14,10 @@ const testFileToPort = new Map();
|
|||
for (let i = 0; i < testFiles.length; i++) {
|
||||
const file = testFiles[i];
|
||||
if (file.endsWith('.test.js')) {
|
||||
// Port 4045 is an unsafe port in Chrome, so skip it.
|
||||
if((4000 + i) === 4045) {
|
||||
i++;
|
||||
}
|
||||
testFileToPort.set(file, 4000 + i);
|
||||
}
|
||||
}
|
||||
|
|
31
packages/astro/src/cli/create-key/index.ts
Normal file
31
packages/astro/src/cli/create-key/index.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { type Flags, flagsToAstroInlineConfig } from '../flags.js';
|
||||
import { createNodeLogger } from '../../core/config/logging.js';
|
||||
import { createKey as createCryptoKey,encodeKey } from '../../core/encryption.js';
|
||||
|
||||
interface CreateKeyOptions {
|
||||
flags: Flags;
|
||||
}
|
||||
|
||||
export async function createKey({ flags }: CreateKeyOptions): Promise<0 | 1> {
|
||||
try {
|
||||
const inlineConfig = flagsToAstroInlineConfig(flags);
|
||||
const logger = createNodeLogger(inlineConfig);
|
||||
|
||||
const keyPromise = createCryptoKey();
|
||||
const key = await keyPromise;
|
||||
const encoded = await encodeKey(key);
|
||||
|
||||
logger.info('crypto', `Generated a key to encrypt props passed to Server islands. To reuse the same key across builds, set this value as ASTRO_KEY in an environment variable on your build server.
|
||||
|
||||
ASTRO_KEY=${encoded}`);
|
||||
} catch(err: unknown) {
|
||||
if(err != null) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err.toString());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -7,6 +7,7 @@ type CLICommand =
|
|||
| 'help'
|
||||
| 'version'
|
||||
| 'add'
|
||||
| 'create-key'
|
||||
| 'docs'
|
||||
| 'dev'
|
||||
| 'build'
|
||||
|
@ -30,6 +31,7 @@ async function printAstroHelp() {
|
|||
['add', 'Add an integration.'],
|
||||
['build', 'Build your project and write it to disk.'],
|
||||
['check', 'Check your project for errors.'],
|
||||
['create-key', 'Create a cryptography key'],
|
||||
['db', 'Manage your Astro database.'],
|
||||
['dev', 'Start the development server.'],
|
||||
['docs', 'Open documentation in your web browser.'],
|
||||
|
@ -78,6 +80,7 @@ function resolveCommand(flags: yargs.Arguments): CLICommand {
|
|||
'build',
|
||||
'preview',
|
||||
'check',
|
||||
'create-key',
|
||||
'docs',
|
||||
'db',
|
||||
'info',
|
||||
|
@ -111,6 +114,11 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
|
|||
await printInfo({ flags });
|
||||
return;
|
||||
}
|
||||
case 'create-key': {
|
||||
const { createKey } = await import('./create-key/index.js');
|
||||
const exitCode = await createKey({ flags });
|
||||
return process.exit(exitCode);
|
||||
}
|
||||
case 'docs': {
|
||||
const { docs } = await import('./docs/index.js');
|
||||
await docs({ flags });
|
||||
|
|
|
@ -23,7 +23,7 @@ import { resolveConfig } from '../config/config.js';
|
|||
import { createNodeLogger } from '../config/logging.js';
|
||||
import { createSettings } from '../config/settings.js';
|
||||
import { createVite } from '../create-vite.js';
|
||||
import { createKey } from '../encryption.js';
|
||||
import { createKey, getEnvironmentKey, hasEnvironmentKey } from '../encryption.js';
|
||||
import type { Logger } from '../logger/core.js';
|
||||
import { levels, timerMessage } from '../logger/core.js';
|
||||
import { apply as applyPolyfill } from '../polyfill.js';
|
||||
|
@ -188,6 +188,9 @@ class AstroBuilder {
|
|||
green(`✓ Completed in ${getTimeStat(this.timer.init, performance.now())}.`),
|
||||
);
|
||||
|
||||
const hasKey = hasEnvironmentKey();
|
||||
const keyPromise = hasKey ? getEnvironmentKey() : createKey();
|
||||
|
||||
const opts: StaticBuildOptions = {
|
||||
allPages,
|
||||
settings: this.settings,
|
||||
|
@ -198,7 +201,7 @@ class AstroBuilder {
|
|||
pageNames,
|
||||
teardownCompiler: this.teardownCompiler,
|
||||
viteConfig,
|
||||
key: createKey(),
|
||||
key: keyPromise,
|
||||
};
|
||||
|
||||
const { internals, ssrOutputChunkNames, contentFileNames } = await viteBuild(opts);
|
||||
|
|
|
@ -20,6 +20,35 @@ export async function createKey() {
|
|||
return key;
|
||||
}
|
||||
|
||||
// The environment variable name that can be used to provide the encrypted key.
|
||||
const ENVIRONMENT_KEY_NAME = 'ASTRO_KEY' as const;
|
||||
|
||||
/**
|
||||
* Get the encoded value of the ASTRO_KEY env var.
|
||||
*/
|
||||
export function getEncodedEnvironmentKey(): string {
|
||||
return process.env[ENVIRONMENT_KEY_NAME] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the environment variable key ASTRO_KEY is set.
|
||||
*/
|
||||
export function hasEnvironmentKey(): boolean {
|
||||
return getEncodedEnvironmentKey() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the environment variable key and decode it into a CryptoKey.
|
||||
*/
|
||||
export async function getEnvironmentKey(): Promise<CryptoKey> {
|
||||
// This should never happen, because we always check `hasEnvironmentKey` before this is called.
|
||||
if(!hasEnvironmentKey()) {
|
||||
throw new Error(`There is no environment key defined. If you see this error there is a bug in Astro.`)
|
||||
}
|
||||
const encodedKey = getEncodedEnvironmentKey();
|
||||
return decodeKey(encodedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a key that has been serialized to an array of bytes and returns a CryptoKey
|
||||
*/
|
||||
|
|
|
@ -18,6 +18,7 @@ export type LoggerLabel =
|
|||
| 'check'
|
||||
| 'config'
|
||||
| 'content'
|
||||
| 'crypto'
|
||||
| 'deprecated'
|
||||
| 'markdown'
|
||||
| 'router'
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import svelte from '@astrojs/svelte';
|
||||
import { defineConfig } from 'astro/config';
|
||||
import testAdapter from '../../../test-adapter.js';
|
||||
|
||||
export default defineConfig({
|
||||
adapter: testAdapter(),
|
||||
output: 'server',
|
||||
integrations: [
|
||||
svelte()
|
||||
|
|
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -1615,6 +1615,15 @@ importers:
|
|||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
|
||||
packages/astro/e2e/fixtures/server-islands-key:
|
||||
dependencies:
|
||||
'@astrojs/node':
|
||||
specifier: ^8.3.3
|
||||
version: 8.3.3(astro@packages+astro)
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/e2e/fixtures/solid-circular:
|
||||
dependencies:
|
||||
'@astrojs/solid-js':
|
||||
|
|
Loading…
Add table
Reference in a new issue