0
Fork 0
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:
Matthew Phillips 2024-09-06 12:41:39 -04:00 committed by GitHub
parent e55c668b25
commit bd1d4aaf82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 189 additions and 2 deletions

View 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=
```

View file

@ -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,
}
});

View 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"
}
}

View file

@ -0,0 +1,6 @@
---
const { secret } = Astro.props;
---
<h2 id="island">I am an island</h2>
<slot />
<h3 id="secret">{secret}</h3>

View file

@ -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>

View 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');
});
});
});

View file

@ -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);
}
}

View 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;
}

View file

@ -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 });

View file

@ -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);

View file

@ -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
*/

View file

@ -18,6 +18,7 @@ export type LoggerLabel =
| 'check'
| 'config'
| 'content'
| 'crypto'
| 'deprecated'
| 'markdown'
| 'router'

View file

@ -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
View file

@ -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':