Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support cloudflare access #348

Merged
merged 13 commits into from
Oct 29, 2024
4 changes: 2 additions & 2 deletions docs/content/0.index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ hero:
light: '/images/landing/hero-light.svg'
dark: '/images/landing/hero-dark.svg'
headline:
label: "Automatic Database Migrations"
to: /changelog/database-migrations
label: "Cloudflare Access integration"
to: /changelog/cloudflare-access
icon: i-ph-arrow-right
features:
- name: Cloud Hosting
Expand Down
116 changes: 116 additions & 0 deletions docs/content/1.docs/3.recipes/6.cloudflare-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
title: Cloudflare Access Integration
navigation.title: Cloudflare Access
description: Learn how to use Cloudflare Access to protect your Nuxt application deployed on Cloudflare Pages.
---

[Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) allows you to secure your web applications by restricting who can reach your application by applying configured identity-aware Access policies. Cloudflare Access is part of [Cloudflare's Zero Trust](https://www.cloudflare.com/plans/zero-trust-services/) offerings.

NuxtHub fully supports Cloudflare Access across the NuxtHub admin, module and CLI.

## Setup Cloudflare Access

These steps covers setting up Cloudflare Access for a deployed NuxtHub project.

::important{to="#nuxtdev-subdomain-with-cloudflare-access"}
When using Cloudflare Access with NuxtHub, the nuxt.dev subdomain is unavailable due to a Cloudflare limitation. [Learn more](#nuxtdev-subdomain-with-cloudflare-access).
::

1. Create a Cloudflare Access [service token](https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/) in the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/)
1. In [Zero Trust](https://one.dash.cloudflare.com/), go to Access → Service Auth → Service Tokens
2. Select Create Service Token
3. Name the service token. For example, the NuxtHub project's name
- The name allows you to easily identify events related to the token in the logs
4. Choose a Service Token Duration. This sets the expiration date for the token
5. Select Generate token. You will see the generated Client ID and Client Secret for the service token
::warning
This is the only time Cloudflare Access will display the Client Secret. If you lose the Client Secret, you will need to rotate the service token
::
2. Configure the Cloudflare Access integration within [NuxtHub admin](https://admin.hub.nuxt.com/)
1. In the [NuxtHub admin](https://admin.hub.nuxt.com/), go to Projects → Settings → General → Cloudflare Access
2. Provide the Client ID and Client Secret generated in the previous step
3. Enable Cloudflare Access on the Pages project
1. On the [Cloudflare dashboard](https://dash.cloudflare.com/login?to=/:account/workers-and-pages) → Workers & Pages → Your Pages project
2. Go to Settings → General → Access Policy
3. Select Enable to create a Cloudflare Access application.
::note
The default policy covers the preview environments on the `pages.dev` subdomain, adds an allow policy with all members of the account, and uses the email one-time-pin IdP.
::
4. Add an Access policy to permit the service token
1. In [Zero Trust](https://one.dash.cloudflare.com/), go to Access → Applications and select the application
2. Create a new policy with the name "NuxtHub" and the action Service Auth
3. Enable the 401 Response boolean
4. Within Configure rules, set Selector to Service Token and the value to the service token created earlier
5. Save the policy
5. Optionally edit your Allow Access policy
::callout{to="https://developers.cloudflare.com/cloudflare-one/policies/access/#allow"}
Learn more about Cloudflare Access policies on Cloudflare's documentation.
::
6. Optionally add additional domains to your Access application
1. In [Zero Trust](https://one.dash.cloudflare.com/), go to Access → Applications and select the application
2. From the header, select Overview
3. Add additional application domains, such as the production domain, or any custom domains assigned to the project

### Importing Pages projects

We plan to directly support importing existing Pages projects that are protected with Cloudflare Access enabled in the future.

Currently, you will need to temporarily create an Access application which sets a Bypass policy for Everyone on the project's default pages.dev domain and the path `/api/_hub/manifest`.

## Service token expiry and rotation

### Service token expiry

When a service token is expired, it can be rotated from the Cloudflare dashboard.

1. In [Zero Trust](https://one.dash.cloudflare.com/), go to Access → Service Auth → Service Tokens
2. Click `...` on the expired token
3. Select Rotate

::tip
The duration of active service tokens can be extended by refreshing it from the Zero Trust dashboard
::

### Service token rotation

If a service token is rotated, the new Client Secret needs to be provided on [NuxtHub admin](https://admin.hub.nuxt.com/).
1. In the [NuxtHub admin](https://admin.hub.nuxt.com/), go to Projects → Settings → General → Cloudflare Access
2. Click Disable integration
3. Provide the Client ID and Client Secret generated in the previous step
4. Click Enable integration

## Remote storage

These steps will cover using [remote storage](/docs/getting-started/remote-storage) with an environment protected by Cloudflare Access.

1. Open `.env`
2. Set the following variables with your service token's Client ID and Client Secret
- `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID`
- `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET`

::note
A separate service token can optionally be created only for local development
::


::tip
No configuration is required if using [Cloudflare WARP](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/) with Cloudflare Zero Trust, as WARP handles authenticating with Cloudflare Access
::

## CLI

The following environment variables can be passed to the CLI
- `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID`
- `NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET`

```bash [Terminal]
export NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID=""
export NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET=""
npx nuxthub database migrations list --preview
```

## `nuxt.dev` subdomain with Cloudflare Access

Due to a technical Cloudflare limitation, when using Cloudflare Access with NuxtHub, the nuxt.dev subdomain is unavailable.

If the nuxt.dev subdomain is the primary domain, enabling the Cloudflare Access integration will set the primary domain to the pages.dev subdomain.
32 changes: 32 additions & 0 deletions docs/content/4.changelog/cloudflare-access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: Cloudflare Access integration
description: "We now support Cloudflare Access within NuxtHub admin, module and CLI"
date: 2024-10-29
image: '/images/changelog/cloudflare-access.png'
authors:
- name: Rihan Arfan
avatar:
src: https://avatars.githubusercontent.com/u/20425781?v=4
to: https://x.com/RihanArfan
username: RihanArfan
---

::tip
This feature is available on all [NuxtHub plans](/pricing) and comes with the [v0.8.4](https://github.com/nuxt-hub/core/releases/tag/v0.8.4) release of `@nuxthub/core`.
::

We now fully support Cloudflare Access across admin, module and CLI.

## What is Cloudflare Access

:nuxt-img{src="/images/changelog/cloudflare-access.png" alt="Cloudflare Access" width="915" height="515"}

[Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) allows you to secure your web applications by restricting who can reach your application by applying configured identity-aware Access policies. Cloudflare Access is part of [Cloudflare's Zero Trust](https://www.cloudflare.com/plans/zero-trust-services/) offerings.

## What does this mean for NuxtHub

This enables you to create secure internal web applications on NuxtHub, without compromising on features like NuxtHub admin for management and remote storage during development.

::callout{to="/docs/recipes/cloudflare-access" icon="i-ph-book-open-duotone"}
Learn more about enabling Cloudflare Access with NuxtHub.
::
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { joinURL } from 'ufo'
import { defu } from 'defu'
import { $fetch } from 'ofetch'
import { addDevToolsCustomTabs } from './utils/devtools'
import { getCloudflareAccessHeaders } from './runtime/utils/cloudflareAccess'

const log = logger.withTag('nuxt:hub')
const { resolve, resolvePath } = createResolver(import.meta.url)
Expand All @@ -21,6 +22,10 @@ export interface HubConfig {
userToken?: string
env?: string
version?: string
cloudflareAccess?: {
clientId: string
clientSecret: string
}

ai?: boolean
analytics?: boolean
Expand Down Expand Up @@ -332,7 +337,8 @@ export async function setupRemote(_nuxt: Nuxt, hub: HubConfig) {
const remoteManifest = hub.remoteManifest = await $fetch<HubConfig['remoteManifest']>('/api/_hub/manifest', {
baseURL: hub.projectUrl as string,
headers: {
authorization: `Bearer ${hub.projectSecretKey || hub.userToken}`
authorization: `Bearer ${hub.projectSecretKey || hub.userToken}`,
...getCloudflareAccessHeaders(hub.cloudflareAccess)
}
})
.catch(async (err) => {
Expand Down
5 changes: 5 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export default defineNuxtModule<ModuleOptions>({
hyperdrive: {},
// @ts-expect-error nitro.cloudflare.wrangler is not yet typed
compatibilityFlags: nuxt.options.nitro.cloudflare?.wrangler?.compatibility_flags
},
// Cloudflare Access
cloudflareAccess: {
clientId: process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_ID || null,
clientSecret: process.env.NUXT_HUB_CLOUDFLARE_ACCESS_CLIENT_SECRET || null
}
})
runtimeConfig.hub = hub
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/ai/server/utils/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createError } from 'h3'
import type { H3Error } from 'h3'
import type { Ai } from '@cloudflare/workers-types/experimental'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

let _ai: Ai
Expand All @@ -30,7 +31,8 @@ export function hubAI(): Ai {
// @ts-expect-error globalThis.__env__ is not defined
const binding = process.env.AI || globalThis.__env__?.AI || globalThis.AI
if (hub.remote && hub.projectUrl && !binding) {
_ai = proxyHubAI(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
_ai = proxyHubAI(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
} else if (import.meta.dev) {
// Mock _ai to call NuxtHub Admin API to proxy CF account & API token
_ai = {
Expand Down Expand Up @@ -72,6 +74,7 @@ export function hubAI(): Ai {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const ai = proxyHubAI('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -82,14 +85,15 @@ export function hubAI(): Ai {
*
* @see https://developers.cloudflare.com/workers-ai/configuration/bindings/#methods
*/
export function proxyHubAI(projectUrl: string, secretKey?: string): Ai {
export function proxyHubAI(projectUrl: string, secretKey?: string, headers?: HeadersInit): Ai {
requireNuxtHubFeature('ai')

const aiAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/ai'),
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
return {
Expand Down
9 changes: 6 additions & 3 deletions src/runtime/analytics/server/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ofetch } from 'ofetch'
import { joinURL } from 'ufo'
import { createError } from 'h3'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

const _datasets: Record<string, AnalyticsEngineDataset> = {}
Expand Down Expand Up @@ -45,7 +46,8 @@ export function hubAnalytics() {
const hub = useRuntimeConfig().hub
const binding = getAnalyticsBinding()
if (hub.remote && hub.projectUrl && !binding) {
return proxyHubAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
return proxyHubAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
}
const dataset = _useDataset()

Expand All @@ -57,13 +59,14 @@ export function hubAnalytics() {
}
}

export function proxyHubAnalytics(projectUrl: string, secretKey?: string) {
export function proxyHubAnalytics(projectUrl: string, secretKey?: string, headers?: HeadersInit) {
requireNuxtHubFeature('analytics')

const analyticsAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/analytics'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
10 changes: 7 additions & 3 deletions src/runtime/blob/server/utils/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { joinURL } from 'ufo'
import type { BlobType, FileSizeUnit, BlobUploadedPart, BlobListResult, BlobMultipartUpload, HandleMPUResponse, BlobMultipartOptions, BlobUploadOptions, BlobPutOptions, BlobEnsureOptions, BlobObject, BlobListOptions, BlobCredentialsOptions, BlobCredentials } from '@nuxthub/core'
import { streamToArrayBuffer } from '../../../utils/stream'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

const _r2_buckets: Record<string, R2Bucket> = {}
Expand Down Expand Up @@ -166,7 +167,8 @@ export function hubBlob(): HubBlob {
const hub = useRuntimeConfig().hub
const binding = getBlobBinding()
if (hub.remote && hub.projectUrl && !binding) {
return proxyHubBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
return proxyHubBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
}
const bucket = _useBucket()

Expand Down Expand Up @@ -350,6 +352,7 @@ export function hubBlob(): HubBlob {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const blob = proxyHubBlob('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -358,13 +361,14 @@ export function hubBlob(): HubBlob {
*
* @see https://hub.nuxt.com/docs/features/blob
*/
export function proxyHubBlob(projectUrl: string, secretKey?: string): HubBlob {
export function proxyHubBlob(projectUrl: string, secretKey?: string, headers?: HeadersInit): HubBlob {
requireNuxtHubFeature('blob')

const blobAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/blob'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
6 changes: 4 additions & 2 deletions src/runtime/cache/server/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ export function hubCacheBinding(name: string = 'CACHE'): KVNamespace {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const cache = proxyHubCache('https://my-deployed-project.nuxt.dev', 'my-secret-key')
* const caches = await cache.list()
* ```
*
*/
export function proxyHubCache(projectUrl: string, secretKey?: string) {
export function proxyHubCache(projectUrl: string, secretKey?: string, headers?: HeadersInit) {
requireNuxtHubFeature('cache')

const cacheAPI = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/cache'),
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})

Expand Down
10 changes: 7 additions & 3 deletions src/runtime/database/server/utils/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createError } from 'h3'
import type { H3Error } from 'h3'
import type { D1Database } from '@nuxthub/core'
import { requireNuxtHubFeature } from '../../../utils/features'
import { getCloudflareAccessHeaders } from '../../../utils/cloudflareAccess'
import { useRuntimeConfig } from '#imports'

let _db: D1Database
Expand All @@ -28,7 +29,8 @@ export function hubDatabase(): D1Database {
// @ts-expect-error globalThis.__env__ is not defined
const binding = process.env.DB || globalThis.__env__?.DB || globalThis.DB
if (hub.remote && hub.projectUrl && !binding) {
_db = proxyHubDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken)
const cfAccessHeaders = getCloudflareAccessHeaders(hub.cloudflareAccess)
_db = proxyHubDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken, cfAccessHeaders)
return _db
}
if (binding) {
Expand All @@ -43,6 +45,7 @@ export function hubDatabase(): D1Database {
*
* @param projectUrl The project URL (e.g. https://my-deployed-project.nuxt.dev)
* @param secretKey The secret key to authenticate to the remote endpoint
* @param headers The headers to send with the request to the remote endpoint
*
* @example ```ts
* const db = proxyHubDatabase('https://my-deployed-project.nuxt.dev', 'my-secret-key')
Expand All @@ -51,14 +54,15 @@ export function hubDatabase(): D1Database {
*
* @see https://hub.nuxt.com/docs/features/database
*/
export function proxyHubDatabase(projectUrl: string, secretKey?: string): D1Database {
export function proxyHubDatabase(projectUrl: string, secretKey?: string, headers?: HeadersInit): D1Database {
requireNuxtHubFeature('database')

const d1API = ofetch.create({
baseURL: joinURL(projectUrl, '/api/_hub/database'),
method: 'POST',
headers: {
Authorization: `Bearer ${secretKey}`
Authorization: `Bearer ${secretKey}`,
...headers
}
})
return {
Expand Down
Loading
Loading