Remix+CloudflareでWebサイトを作る 4(Access Policyでアクセス制限をかける・noindex・DNSSEC・Basic認証の環境変数設定)
【2024-02-08】Cloudlfare Pagesでデプロイして一時的に作成されたURLにアクセス制限をかける
背景・経緯
Cloudlfare Pagesにはプロダクションとプレビュー(Staging)の2つの環境が存在する。
プレビューに反映されるのはmain ブランチにpushされた時、という設定は最初にした。
それ以外にpushするとプレビュー環境へのデプロイになる。
そしてどちらの環境でもデプロイ時に 1234abcdef.YOUR_PROJECT_NAME.pages.dev みたいな適当なURLが生成されプロダクション環境の最新版のURLが YOUR_PROJECT_NAME.pages.dev のURLとして利用される。
作りながら「この適当なURLへのアクセスへの制限をかけておきたいな」と思ったので調べた。
方法
Cloudflare Access により、プロジェクトのプレビュー環境のデプロイへのアクセスを制御します。
と書いているので少なくともプレビューの一時的に作成されているページにはアクセス制限もかけられるっぽい。
公式ドキュメント に書いているが、「Workers & Pages > プロジェクト選択 > 設定 > 一般 > Accessポリシー」で「アクセスポリシーを有効にする」というボタンをクリックするとプロジェクトにメンバーに追加されている人以外は見れないようにできる。
試しに 1234abcdef.YOUR_PROJECT_NAME.pages.dev (URLは適当)のようなページにアクセスすると以下のようになる。
Web上で設定の無効化は できない
どうやら一度有効にすると無効にすることがWeb上ではできない。
ここに削除方法が書いてあったが、結論2回curlを実行する必要がある。
なぜWeb上で無効にできない仕様なのか謎。
- 手順1: 「アカウントID」「API Key」「メールアドレス」を準備してcurlを実行し app IDを取得
- 手順2: app IDをcurlのエンドポイントとして利用してポリシーを無効にする
【2024-02-09】ドメインを取得したその後 1:noindex付与
確認してみる
ここでドメインを取得してカスタムドメインのセットアップを行った。
「DNSセットアップを実行」をクリックして、CNAMEなどを入力する欄があったがフォームをクリックしても編集できなかったのでとりあえず検証を開始ボタンをクリック。
最大48時間と書いていたが10分くらいで https://YOUR_PROJECT_NAME.app/ にアクセスしたらページが表示されるようになった。
元のURLを無効にしたい
結論:不可能
https://YOUR_PROJECT_NAME.app/ というURLは「https://YOUR_PROJECT_NAME.pages.dev/ と同じもの」という位置づけで作成しただけなので、https://YOUR_PROJECT_NAME.pages.dev/ が無効になっているわけではない。
https://YOUR_PROJECT_NAME.pages.dev/ にアクセスしても無効になるようにしたい。
この記事によると無理っぽい。
複数のURLでアクセスできてしまうと、重複コンテンツとみなされてしまいSEO的に不利になる
しかしながら2つ存在しているのはSEO的には不利と。なるほど。
仕方ないので、pages.devドメインはgoogleにindexされないように対策をします。
直でURL叩いたら表示されるのはもうしゃーなし、と。
一旦ググった時に重複しないように設定しようということか。
どっちのURLの/articles/2
にアクセスしたとしてもアクセスカウンターインクリメントされることになるのか。この挙動は正しいか。なんかキモいけど。
メタデータの書き方
ディレクティブ
SEO対策のためにnoindexとかいうのを設定するらしいがそこらへんのやつわからないので調べる。
こいつらは、「ディレクティブ」と呼ばれるもので、特定のページにメタタグで指定することで、検索エンジンのクローラに対してページの処理を指示している。
以下な形式で robots
を指定することでいろいろな内容を指示できる。
<meta name="robots" content="noindex">
content
に指定できるディレクティブには以下のようなものがある。
- noindex
- 検索結果に出す必要の無い、または出したくないページに付与
- nofollow
- ページ上の配置されているリンクを辿らないようにクローラーへヒントを伝える
- 強制力があるわけではない
- こちらも検索結果に自分のページを表示させたくない時に使用できる
- noarchive
- 検索エンジンデータベースへの保存の拒否
- none
- インデックスとリンク先へのクロールの両方を拒否
今回の場合は https://YOUR_PROJECT_NAME.pages.dev/ 側に noindex を付与する。
Referrer-Policy
Cloudflareでの書き方を色々調べながらこのページをみたら、リファラーを指定するということもよく利用される例として書かれていた。
この方Cloudlfareのこと調べてると何回もでてくる。ありがたい🙇🏻♂️
以下のようにメタタグに書かれる。
<meta name="referrer" content="strict-origin-when-cross-origin" />
- そもそもRefererとは
- HTTPヘッダのひとつで、どのページからアクセスが来たのかの情報を渡す
- 例:Google検索から流入した場合HTTPのリクエストヘッダに
Referer: https://www.google.com/
がついているのでGoogleから来たことがわかる - "Referrer" が正しいスペルだが "Referer" になっているのは策定時にタイポしたかららしい...
- どのように送られるか
- ブラウザがリクエストヘッダーに情報を付与している
- 脆弱性
- URLのクエリにメアドやパスワードなどの重要な情報が含まれている場合、遷移先サイトに送られてしまう
- URLにセキュアな情報を入れない方が良いが認証でのリダイレクトなどでどうしても入れたい場合がある
- これを回避するための方法として利用されるのがReferrer-Policy
- Referrer-Policyとは
- メタタグに指定することでRefererを送信するブラウザの挙動を変更するもの
-
no-referer
を指定すれば全く送らないようにすることもできる
- Referrer-Policyのベストプラクティス
- そもそもリファラーで何を実現したい・したくないのか
-
- 外部サイトには全てのURLを送りたくない
-
- 同じサイト内での遷移は全てのURLを送りたい(tracking等便利)
-
- HTTPへリクエストを送るときはrefererを送りたくない(中間者攻撃を防ぐ)
-
- となるとstrict-origin-when-cross-originになる。デフォルトの設定だとこれが指定されている(ChromeのHeaderで確認できた)
-
target=_blank
の脆弱性の対応策として使用することもできる
- そもそもリファラーで何を実現したい・したくないのか
その他参考
- noindex/nofollowの使い方とX-Robots-Tag | Search Times
- 知ってるようで知らないRefererとReferrer-Policyのお話 #Security - Qiita
SEO対策ヤッテミル
知らないことが多くて調べる時間が長くなったが https://YOUR_PROJECT_NAME.pages.dev/ が検索結果に表示されないように noindex を付与する。
まずは公式ドキュメントをざっくり読む。
今回の場合はpublic/
以下かなと思って見てみると既に public/_headers
というファイルが存在して Cache-Control:
の設定が書かれていたのでここに X-Robots-Tag: noindex
を追加して終わり。
Commitしてデプロイされたら元の参考記事のようにResponse Headersにの中にnoindex
があることを確認した(pages.dev
を選択してもResponse Headersの中には無かったがその下のjsファイルの中に存在した)。
【2024-02-10】ドメインを取得したその後 2:セキュリティとパフォーマンスの最適化
クイックスタート
「Webサイト > 任意のアプリ」をクリックするとドメインを取得したアプリの概要が見れて、クイックスタートガイドが存在するのでやってみる。
「設定をレビュー」をクリック
「①」をクリックして「ヘルプ」をクリックすると以下のようになる。優しい。
「保存」をクリックして続けていく。ヘルプ助かる。
全部ONにして終わり。
DNS設定
次は「DNS > 設定」からDNSSECの設定をする。
なんだこれ。
DNSSEC(ディーエヌエスセック)とは
その前にまず、「DNSキャッシュポイズニング」というものがある。
これはキャッシュDNSサーバーに情報を登録して偽のサイトに誘導する攻撃。
キャッシュDNSサーバーはトランザクションIDとポート番号で、応答の信頼性を検証する仕組みがあるが、キャッシュDNSサーバーに正規のDNSサーバーよりも早くレスポンス(偽の情報)を返すと低確率だが攻撃される可能性がある。
つまり、根本的にDNSキャッシュポイズニングを防げるようにはなっていない
ここでDNSSEC。
コンテンツDNSサーバーが応答を返す時に署名を付与することで正当性を保証する。
参考: #46【サクッと学べる支援士対策】DNSSEC - YouTube
DNSSEC自体は2010年くらいからあるらしいけどクライアント・サーバーでの設定が難しくてあまり普及してなかったっぽい(2023年現在、世界では30%、日本では14% by 上のYoutube)。
こういうの聞くと有り難みが増す。
Cloudflareはボタンぽちーで済むの助かった🙏🏻
なんか色々できそうだしドキュメントも充実してそうだから少しずついじっていきたい。
【2024-02-11】コードにベタ書きしていたBasic認証の情報をCloudflareで管理する
背景
Basic認証の情報ベタ書きしてたけどどうにかしたいな。
Cloudflareの管理画面で「環境変数」ってとこあるしそこかな?
ベストプラクティスを知りたい。
手順
Secrets · Cloudflare Workers docs を参照した。
.dev.vars
を作成し、 username
password
の値を書き込む
1. 開発環境
BASIC_AUTH_USERNAME=username
BASIC_AUTH_PASSWORD=password
npx create-remix@latest --template remix-run/remix/templates/cloudflare-pages
で作成すると.gitignore
に .dev.vars
がデフォルトで追加されていたがなければ追記すること。
プレビュー・プロダクション環境
npx wrangler secret put <KEY>
を実行するか、以下のようにWeb上から登録するかの2パターン
remix.env.d.ts
に以下を追加
2. interface Env {
// .dev.vars
BASIC_AUTH_USERNAME: string; // ここを追加
BASIC_AUTH_PASSWORD: string // ここを追加;
// wrangler.toml
DB: D1Database;
}
npm run dev
で立ち上げるとサーバーのログに以下のように表示される
✨ Compiled Worker successfully
⛅️ wrangler 3.26.0 (update available 3.28.1)
-------------------------------------------------------
Using vars defined in .dev.vars
Your worker has access to the following bindings:
- D1 Databases:
- DB: hoge (hogehogehogehogehogehoge)
- Vars:
- BASIC_AUTH_USERNAME: "(hidden)"
- BASIC_AUTH_PASSWORD: "(hidden)"
3. loader/actionに記述
これでアクセスできるようになっている。
export async function loader({ context }: LoaderFunctionArgs) {
const env = context.env as Env;
console.log(env.BASIC_AUTH_USERNAME);
console.log(env.BASIC_AUTH_PASSWORD);
}
ところでBasic認証のパスワードの最大の長さは?
気になったので調べてみた。
結論:パスワードは8文字
8文字以上より長いパスワードにしても先頭8文字しか見ていない。
理由はパスワードの暗号化方式がDES方式だから。
これは8文字までのハッシュ方式で他のに比べて脆弱らしい。
他のハッシュ方式は以下のようなものがあるらしい。
詳しいところまでは調べず。
- MD5ハッシュ:9文字以上可能
- SHAハッシュ:9文字以上可能
- Blowfish:9文字以上可能で強力らしい
修正するためには暗号化方式を変えれば良い。
【2024-02-11】GrapiQL使おうとした(使えなかった)
経緯
多分そのうち記事のアクセスランキングとか取りたくなるだろうからCloudflare Analytics使ってみよう。
そのためにはGraphQLを使用しなくてはならないので、公式ドキュメントを見ながら進めていた
Configure GraphQL client endpoint and HTTP headers · Cloudflare Analytics docs
適当なページを作って試してみる
GraphiQLの公式リポジトリ内にあるexampleコードを見て適当なページに適用してみた。
import { GraphiQL } from "graphiql";
import type { Fetcher } from "@graphiql/toolkit";
import "graphiql/graphiql.min.css";
const fetcher: Fetcher = async (graphQLParams) => {
const data = await fetch(
"https://swapi-graphql.netlify.app/.netlify/functions/index",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(graphQLParams),
credentials: "same-origin",
}
);
return data.json().catch(() => data.text());
};
export default function IndexPage() {
return (
<div>
<GraphiQL fetcher={fetcher} />
</div>
);
}
エラー:Node builtin "punycode"
このようなエラーがでたので言われているとおりに修正
✘ [ERROR] Node builtin "punycode" (imported by "node_modules/markdown-it/lib/index.js") must be polyfilled for the browser. You can enable this polyfill in your Remix config, e.g. `browserNodeBuiltinsPolyfill: { modules: { punycode: true } }` [plugin browser-node-builtins-polyfill-plugin]
/** @type {import('@remix-run/dev').AppConfig} */
export default {
...
// ここを追加
browserNodeBuiltinsPolyfill: {
modules: {
punycode: true,
},
},
};
punycode
とは何か?Copilotに聞いてみた。
punycodeは、Unicode文字列をASCII文字列に変換するためのエンコーディング方式です。これは主に、国際化ドメイン名(IDN)をASCII文字列に変換するために使用されます。
インターネットの初期の設計では、ドメイン名はASCII文字のみを使用していました。しかし、世界中の多くの言語はASCII文字セットに含まれていない文字を使用しています。これらの言語をサポートするために、国際化ドメイン名(IDN)が導入されました。IDNでは、Unicode文字を使用してドメイン名を表現することができます。
しかし、インターネットの基盤となる多くのプロトコルは、ASCII文字のみをサポートしています。そのため、Unicode文字を含むドメイン名をこれらのプロトコルで使用するためには、何らかの方法でASCII文字に変換する必要があります。これがpunycodeの役割です。
例えば、ドメイン名
例え.jp
は、punycodeを使用してxn--r8jz45g.jp
に変換されます。この変換されたドメイン名は、ASCII文字のみを使用しているため、既存のインターネットプロトコルで使用することができます。
Node.jsのpunycodeモジュールは、このpunycodeエンコーディングを扱うためのユーティリティを提供します。
エラー:ReferenceError: window is not defined
<GraphiQL fetcher={fetcher} />
でこのようなエラーが発生。
色々調べていじくってみたが治らないので一旦スキップする。急いでいることでもないし。
後でちゃんと動くようにする。
✘ [ERROR] ReferenceError: window is not defined
at store (file:///app_name/node_modules/@graphiql/react/src/utility/debounce.ts:14:15)
at storeTabs (file:///app_name/node_modules/@graphiql/react/src/editor/tabs.ts:250:13)
at null.<anonymous> (file:///app_name/node_modules/@graphiql/react/src/editor/context.tsx:316:5)