Next.jsでContentSecurityPolicyを設定する

ContentSecurityPolicyの概要と、とりあえずの設定内容については下記サイトと記事を読む。 設定内容は下記サイトの Strict CSP ページでおすすめされている内容にする。

expressアプリをラップしてミドルウェアを追加するコードを書く。ソースコードはほぼここで書かれているもの。next.js/examples/with-strict-csp

/* eslint @typescript-eslint/no-var-requires: 0 */
const helmet = require('helmet');
const uuidv4 = require('uuid/v4');

module.exports = function csp(app) {
  // Create a nonce on every request and make it available to other middleware
  app.use((req, res, next) => {
    res.locals.nonce = Buffer.from(uuidv4()).toString('base64');
    next();
  });

  const nonce = (req, res) => `'nonce-${res.locals.nonce}'`;

  const scriptSrc = [nonce, "'strict-dynamic'", "'unsafe-inline'", 'https:'];

  // In dev we allow 'unsafe-eval', so HMR doesn't trigger the CSP
  if (process.env.NODE_ENV !== 'production') {
    scriptSrc.push("'unsafe-eval'");
  }

  const baseDirectives = {
    baseUri: ["'none'"],
    objectSrc: ["'none'"],
    scriptSrc,
  };
  const directives =
    process.env.NODE_ENV === 'production'
      ? { ...baseDirectives, reportUri: process.env.REPORT_URI }
      : baseDirectives;

  app.use(
    helmet({
      contentSecurityPolicy: {
        directives,
        reportOnly: true,
      },
    }),
  );
};

インラインスクリプトを許可するけど、nonceトークンを見て確認する感じ。server.jsで実行する。

const csp = require('./csp');

app.prepare().then(() => {
  const server = express();
  csp(server);
  // ...
}

_document.tsxnonceの設定をする。とりあえずテストだからanyで対応。

static async getInitialProps(ctx: any) {
  const initialProps = await Document.getInitialProps(ctx);
  const { nonce } = ctx.res.locals; // 追加

  return { ...initialProps, nonce };
}

render() {
  const { nonce } = this.props as any; // 追加 とりあえず any で

  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript nonce={nonce} /> // 追加
      </body>
    </Html>
  );
}

プライベートサブネットにあるECSのログをCloudWatchで見たい

ECSをプライベートサブネットに設置したが、Nat gatewaysを削除してインターネットから遮断したところ、CloudWatchのログも見れなくなった。 インターネットへの接続無しにCloudWatchへのログ送信方法を調べたところ、VPC Endpointを設定すれば送信できるようだ。 VPC Endpointはプライベートなネットワークを利用してAWS各サービスと接続できるみたい。

このドキュメントに沿って設置。セキュリティグループはVPCから443ポートのアクセスを受けることができるようにする。

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/vpc-endpoints.html

設置サービスは下記4つ。

  • com.amazonaws.ap-northeast-1.logs
  • com.amazonaws.ap-northeast-1.ecs
  • com.amazonaws.ap-northeast-1.ecs-agent
  • com.amazonaws.ap-northeast-1.ecs-telemetry

ログが送信されるようになった。

SSHで多段接続

いつも忘れるので。.ssh/configにこんな感じで書いてあげればいける。

Host mng
  HostName x.xxx.x.xxx
  Port 22
  User username
  IdentityFile ~/.ssh/id_rsa
  
Host frontend
  HostName x.xxx.x.xxx
  Port 22
  User username
  IdentityFile ~/.ssh/id_rsa
  ProxyCommand ssh -W %h:%p mng

AWSのNAT Gatewaysが高いので、Rubyで開始と終了のスクリプトを書いて、こまめに止める

Nat Gatewaysを利用する際は以下の3ステップがある。

  • 固定IPを allocate する
  • gateway を作る
  • route を作る

この3ステップをスクリプトで書く。

作成時

require 'aws-sdk-ec2'
require 'retryable'
require 'yaml'

def set_env
  @env = YAML.load(File.read('./env.yml'))
  puts "set env"
end

def set_ec2_client
  @client ||= Aws::EC2::Client.new(
    access_key_id: @env['access_key_id'],
    secret_access_key: @env['secret_access_key']
  )
  puts "set ec2 client"
end

def allocate_address
  @allocate_address_result = @client.allocate_address
  puts "address allocated"
end

def create_nat_gateway
  @create_nat_gateway_result = @client.create_nat_gateway({
    allocation_id: @allocate_address_result.allocation_id,
    subnet_id: @env["public_subnet_id"],
  })
  puts "nat gateway created"
end

def create_route
  # ception_cb を設定しないと 3 回失敗しても例外が発生しないため
  exception_cb = proc {}
  
  # nat gateway が作成されるまで時間がかかるため
  Retryable.retryable(tries: 3, on: Aws::EC2::Errors::InvalidGatewayIDNotFound, sleep: 30, exception_cb: exception_cb) do
    puts 'creating'
    @client.create_route({
      destination_cidr_block: "0.0.0.0/0",
      gateway_id: @create_nat_gateway_result.nat_gateway.nat_gateway_id,
      route_table_id: @env["route_table_id"],
    })
    puts "route created"
  end
end

def main
  set_env
  set_ec2_client
  allocate_address
  create_nat_gateway
  create_route
end

main

削除時

require 'aws-sdk-ec2'
require 'retryable'
require 'yaml'

def set_env
  @env = YAML.load(File.read('./env.yml'))
  puts "set env"
end

def set_ec2_client
  @client ||= Aws::EC2::Client.new(
    access_key_id: @env['access_key_id'],
    secret_access_key: @env['secret_access_key']
  )
  puts "set ec2 client"
end

def delete_route
  resp = @client.describe_route_tables({ route_table_ids: [@env["route_table_id"]] })
  @nat_gateway_id = resp.route_tables.first.routes.detect { |route| route.destination_cidr_block === "0.0.0.0/0" }.nat_gateway_id
  @client.delete_route({
    destination_cidr_block: "0.0.0.0/0",
    dry_run: false,
    route_table_id: @env["route_table_id"],
  })
  puts "route deleted"
end

def delete_nat_gateway
  @allocation_id = @client.describe_nat_gateways.first.nat_gateways.detect do |gateway|
    gateway&.nat_gateway_id === @nat_gateway_id
  end.nat_gateway_addresses.first.allocation_id
  
  @client.delete_nat_gateway({
    nat_gateway_id: @nat_gateway_id,
  })
  puts "nat gateway deleted"
end

def release_elastic_ip
  # ception_cb を設定しないと 3 回失敗しても例外が発生しないため
  exception_cb = proc {}
  
  # nat gateway が削除されるまで時間がかかるため
  Retryable.retryable(tries: 6, sleep: 30, exception_cb: exception_cb) do
    puts 'releasing'
    @client.release_address({
      allocation_id: @allocation_id,
    })
    puts "public ip released"
  end
end

def main
  set_env
  set_ec2_client
  delete_route
  delete_nat_gateway
  release_elastic_ip
end

main

時間課金だけでなく転送料金もあった気がすので、本当に安くなるのか1,2ヶ月こまめに止めてチェックしてみる。