Laravel プロジェクトで AWS の ELB(ALB) 443(HTTPS) -> EC2 80(HTTP) とした時、 route() などを期待した動作(https)にする

以前に Laravel プロジェクトを AWS の ELB 443(HTTPS) -> EC2 80(HTTP) とした時、強制的に HTTPS にする方法 というのを書きました。

実は、以前の方法である URL::forceSchema('https'); を使うのはあまりよろしくありません。

どうするのが良さそうか

https://github.com/fideloper/TrustedProxy を使う。

laravel 5.5 からはデフォルトで入ってるはず。

5.4 以前を使っている場合は https://github.com/fideloper/TrustedProxy#tldr-setup に従ったインストールしましょう。

$ php artisan vendor:publish --provider="FideloperProxyTrustedProxyServiceProvider"

これで、 ./config/trustedproxy.php が配置されます。

例えば以下のように、信頼されるアクセス元の情報を配列で指定しましょう。

/config/trustedproxy.php
    'proxies' => [
        // '172.31.0.0/16',
        '10.0.0.0/24',
        '10.0.1.0/24',
    ],
     // 全部信頼できるなら以下のように指定しても OK らしい
     # 'proxies' => '*',

これだけ。

そもそも何が起きているか

※前回も書いたけど。

ELB で https(443) を待ち受けて、内部の EC2 に http(80) で転送していると、 Laravel プロジェクト的には http で動作していることになる。

このため、 route() や redirect() などが、 http:// となってしまう。

期待する動作は、何も意識せずに route() などの結果が https:// になって欲しい。

URL::forceSchema('https'); は何が問題なのか

全部の URL が https:// になる。

全て https で処理するなら問題ないが、何らかの理由があって、 http でも処理したい場合に対処できない。

続きを読む

【Rails】ブラウザからS3へ画像を直接アップロード

こちらを参考に, 画像をブラウザからS3へ直接アップロードできるようにしました.

1. AWSの準備 (時間:5分)

IAMでS3アクセス用のユーザを作成します. まずは以下のようにコンソール画面からIAMへアクセス.

ss 2017-09-18 12.44.57.png

次に, 以下のUsersをクリック
ss 2017-09-18 12.48.08.png

Add userをクリック
ss 2017-09-18 12.48.16.png

任意の名前を入力. プログラマティックアクセスにチェックして次へ.
ss 2017-09-18 12.48.37.png

既存ポリシーの中から AmazonS3FullAccessを見つけてチェック.
eee

以上でユーザ作成は完了. 詳細画面でCreate Access keyをクリックして必要なキーを取得する.
aaa

このようにkeyIDとシークレットkeyが表示されるので, メモっておく.
ss 2017-09-18 12.50.27.png

次にS3の設定を行う.

適当なバケットを作成する. 各種設定はデフォルトのままで良い. 作成し終えたら, そのバケットのプロパティを開き, Permissions -> CORS configurationに以下のように記述する. これで終わり.

ssd

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

2. バックエンドでS3ポリシーを作成

自身のRailsアプリに, POSTメソッドで叩ける適当なエンドポイントを作成する.

routes.rbに例えば以下のように記述.

post 'image_upload', to: 'images#upload'

そして, images#uploadを次のように定義する.

class ImagesController < ApplicationController
  # Deviceを使って認証処理をしている場合, 次の1行によって認証を要求できる.
  before_action :authenticate_user!, only: [:upload]

  # 先ほど作ったバケットの名前とアクセスKeyIdとシークレットkey
  S3_BUCKET = 'your-bucket-name'
  AWS_ACCESS_KEY_ID = 'XXX'
  AWS_SECRET_KEY = 'XXX'

  def upload
    # アップロード後のファイル名
    key = 'hogehoge'

    acl = 'public-read'
    ctype = params[:content_type]

    # ポリシー作成
    policy_document = {
      # 1分間のみ有効
      expiration: (Time.now + 1.minute).utc,
      conditions: [
        # アップロード先のS3バケット
        { bucket: S3_BUCKET },
        # ファイルの権限
        { acl: acl },
        # ファイル名
        { key: key },
        # ファイルの形式
        { 'Content-Type' => ctype },
        # アップロード可能なファイルのサイズ
        ['content-length-range', params[:size], params[:size]]
      ]
    }.to_json
    policy = Base64.encode64(policy_document).gsub("\n", '')

    # signatureの作成
    signature = Base64.encode64(
        OpenSSL::HMAC.digest(
            OpenSSL::Digest::Digest.new('sha1'),
            AWS_SECRET_KEY, policy)).gsub('\n', '')

    # アップロードに必要な情報をJSON形式でクライアントに返す
    render json: {
      url: "https://#{S3_BUCKET}.s3.amazonaws.com/",
      form: {
        AWSAccessKeyId: AWS_ACCESS_KEY_ID,
        signature: signature,
        policy: policy,
        key: key,
        acl: acl,
        'Content-Type' => ctype
      }
    }
  end
end

3. フロントエンドでポリシー取得しアップロード実行

画像をアップロードしたいViewに, 以下のようなjavascriptを仕込みます.


画像を投稿します

<div style="width: 500px">
  <form enctype="multipart/form-data" method="post">
    <input type="file" name="userfile" accept="image/*">
  </form>
</div>

<div id="thumbnail" style="max-width: 100px;">
  <img src="/plus.png" id="image_to_upload">
</div>

<button class="btn btn-primary" id="upload">投稿</button>

<script type="text/javascript">
$(function() {
  var file = null;

  // アップロードするファイルを選択
  $('input[type=file]').change(function() {
    file = $(this).prop('files')[0];
    // 画像以外は処理を停止させるためファイル形式をチェック
    if (file.type != 'image/jpeg' && file.type != 'image/png') {
      // 画像でない場合は消す
      var img_src = $('<img>').attr('src', '/plus.png')
      $('#thumbnail').html(img_src);
      file = null
      return;
    }

    // サムネ表示
    var reader = new FileReader();
    reader.onload = function() {
      var img_src = $('<img>').attr('src', reader.result)
      img_src.css('width', '500px');
      $('#thumbnail').html(img_src);
    }
    reader.readAsDataURL(file);
  });

  // アップロードボタンクリック
  $('#upload').click(function(){
    // ファイルが指定されていなければ何も起こらない
    if(!file) {
      return;
    }

    // ポリシーを発行する
    $.ajax({
      url: 'http://localhost:3000/image_upload',
      type: 'POST',
      data: {
        content_type: file.type,
        size: file.size
      }
    })
    .done(function( data, textStatus, jqXHR ) {
      // 取得したポリシーをフォームデータの形に整形する
      var name, fd = new FormData();
      for (name in data.form) if (data.form.hasOwnProperty(name)) {
        fd.append(name, data.form[name]);
      }
      fd.append('file', file); // ファイルを添付

      $.ajax({
        url: data.url,
        type: 'POST',
        dataType: 'json',
        data: fd,
        processData: false,
        contentType: false
      })
      .done(function( data, textStatus, jqXHR ) {
        console.log('success!')
      })
      .fail(function( jqXHR, textStatus, errorThrown ) {
        // アップロード時のエラー
        console.log('error: 2'); 
      });  

    })
    .fail(function( jqXHR, textStatus, errorThrown ) {
      // ポリシー取得時のエラー
      console.log('error: 1');
    });

  });

});
</script>

次のようなUIが出来上がります. 「ファイルを選択」をクリックしてファイルを適当に選択すると, サムネイルが表示されます. この状態で投稿ボタンを押すと画像がS3へ直接アップロードされます.
ss 2017-09-18 13.16.07.png

以上です.

続きを読む

AWS Lambdaを使ってSlackにメッセージを送る

はじめに

  • この記事ではSlackのWebhookを利用して、AWS Lambda経由でメッセージを送るためのサンプルを記載します。

環境について

  • ローカルの環境にはPython 3.6が導入済みであるものとします。
  • また、対応するpipも導入されているものとします。

作成手順

フォルダ構成

  • 下記のようなフォルダ構成を前提とします。
- webhook
  - script.py
  - 必要なライブラリ類

ライブラリの導入

  • ライブラリとしてslackweb(https://pypi.python.org/pypi/slackweb)を利用します。
  • lambdaで外部のライブラリを利用する場合、ソースコードと依存するライブラリを1つのzipにまとめる必要があります。
  • そのため下記のように、これから書くソースコードと同じディレクトリにライブラリをインストールします。
$ pip3 install slackweb -t .

ソースコードの作成

  • 下記のような感じでコードを書きます。
script.py
# slackwebをインポート
import slackweb

# lambdaの処理するhandlerを作成
def lambda_handler(event, context):

    # urlでは取得済みのWEBHOOK URLを入力
    slack = slackweb.Slack(url="YOUR WEBHOOK URL PUTS HERE")
    # メッセージは適当。チャンネル名、ユーザ名も適宜修正すること。
    slack.notify(text="Hello, World!!", channel="#YOUR CHANNEL HERE", username="USERNAME HERE", icon_emoji=":smiling_imp:")
    return "succeeded"

zipの作成

  • zipを作成するときは、親となるディレクトリを含んではいけない点に注意します。
  • macであれば親ディレクトリでなく、全てのファイルを選択した状態で圧縮すればOKです。

Lambdaのデプロイ

  • 基本はそのまま進めていけば難しいことはありませんが、ハンドラのところはファイル名.ハンドラ名にする必要がありますので、今回であればscript.lambda_handlerを指定してください。
  • ここまで終わればあとは実行するだけです!

終わりに

  • ここまでできれば他のAWSサービスと組み合わせたり、コードを拡充するのは簡単ですね!
  • これをベースにbotや処理ツールなどを作成し、素敵なslack生活を送りましょう!僕も頑張ります笑

続きを読む

Webアプリのスマホテストを自動化する

Webブラウザアプリケーションのスマホテストを外部サービスを使い自動化します。
AWS Device FarmRemote TestKit等のサービスが対象になりますが、今回はAWS Device Farmを利用します。
なお、CIツールはプラグインが存在するJenkinsを用います。

必要なもの

Jenkinsのインストール

terraformを用いてEC2上に、Jenkinsをセットアップします。
スクリプトでは、ユーザデータを用いて、Jenkinsのセットアップを実施しています。

参考レポジトリ

bash
$ git clone https://github.com/Thirosue/devicefarm-sample.git
$ cd devicefarm-sample/provisioning/
$ terraform apply -var-file=~/.aws/terraform.tfvars -var 'key_name=[keypair]'
ec2_jenkins.tf
variable "key_name" {}

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners = ["amazon"]

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  filter {
    name   = "block-device-mapping.volume-type"
    values = ["gp2"]
  }
}

resource "aws_instance" "jenkins" {
  ami           = "${data.aws_ami.amazon_linux.id}"
  instance_type = "t2.micro"
  key_name      = "${var.key_name}"
  user_data = <<EOF
IyEvYmluL2Jhc2gKCndnZXQgLU8gL2V0Yy95dW0ucmVwb3MuZC9qZW5raW5zLnJl
cG8gaHR0cDovL3BrZy5qZW5raW5zLWNpLm9yZy9yZWRoYXQvamVua2lucy5yZXBv
CnJwbSAtLWltcG9ydCBodHRwOi8vcGtnLmplbmtpbnMtY2kub3JnL3JlZGhhdC9q
ZW5raW5zLWNpLm9yZy5rZXkKCnl1bSBpbnN0YWxsIC15IGdpdCBqZW5raW5zIGph
dmEtMS44LjAtb3BlbmpkawphbHRlcm5hdGl2ZXMgLS1zZXQgamF2YSAvdXNyL2xp
Yi9qdm0vanJlLTEuOC4wLW9wZW5qZGsueDg2XzY0L2Jpbi9qYXZhCgpjaGtjb25m
aWcgamVua2lucyBvbgovZXRjL2luaXQuZC9qZW5raW5zIHN0YXJ0CgpleGl0IDA=
EOF
}
userdata.sh
#!/bin/bash

wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key

yum install -y git jenkins java-1.8.0-openjdk
alternatives --set java /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

chkconfig jenkins on
/etc/init.d/jenkins start

exit 0

Jenkinsの管理コンソールセットアップ

EC2のPublicIPを確認し、以下にアクセス

http://[IPv4 Public IP]:8080

アクセス後、Jenkinsがロックされているので、指示通り/var/lib/jenkins/secrets/initialAdminPasswordを確認し入力します。

Unlock Jenkins

Device Farm Plugin インストール

Jenkins-プラグインマネージャよりaws-device-farm-pluginをインストール。

DeviceFarmPlugin

DeviceFarm AccessKey/SecretKey設定

Jenkinsの管理画面に用意したIAMユーザのAccessKey/SecretKeyを設定

DeviceFarmIAMSetting

テストコード作成

以下を参考にテストコードを作成する。
参考レポジトリ はAppium+JUnitをgradleでbuildしている。

SampleTest.java
import com.codeborne.selenide.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Platform;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.io.File;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SampleTest {

    private RemoteWebDriver driver;

    @Before
    public void setUp() throws Exception {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setPlatform(Platform.IOS);
        capabilities.setBrowserName("safari");
        driver = new RemoteWebDriver(new URL("http://127.0.0.1:4723/wd/hub"),
                capabilities);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
    }

    @After
    public void tearDown() throws Exception {
        driver.quit();
    }

    public boolean takeScreenshot(final String name) {
        String screenshotDirectory = System.getProperty("appium.screenshots.dir", System.getProperty("java.io.tmpdir", ""));
        System.out.println(screenshotDirectory);
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        return screenshot.renameTo(new File(screenshotDirectory, String.format("%s.png", name)));
    }

    @Test
    public void runTest() throws Exception {
        driver.get("https://www.google.co.jp/");
        Thread.sleep(1000);

        System.out.println(driver.getTitle());
        System.out.println(driver.getPageSource());

        driver.findElement(By.id("lst-ib")).sendKeys("AWS DeviceFarm");
        driver.findElement(By.id("tsbb")).click();
        assertTrue(takeScreenshot("index"));

        assertEquals("AWS DeviceFarm", driver.findElement(By.id("lst-ib")).getAttribute("value"));
        assertTrue(takeScreenshot("result"));
    }
}

build設定

参考レポジトリ はカスタムタスクのinstallZipでDeviceFarmへのuploadファイルを生成する。

スクリーンショット 2017-09-14 時刻 20.28.51.png

task installZip(dependsOn: ["clean", "packageTests", "installDist"]) << {
    new File("build/work").mkdir()
    new File('build/zip.sh') << 'cd build/work; zip -r zip-with-dependencies.zip .'
    copy{
        from "build/install/test/lib/"
        into "build/work/dependency-jars/"
    }
    copy{
        from "build/libs/test-1.0-SNAPSHOT-tests.jar"
        into "build/work/"
    }
    "chmod 755 build/zip.sh".execute().waitFor()
    "build/zip.sh".execute().waitFor()
    copy{
        from "build/work/zip-with-dependencies.zip"
        into "build/"
    }
}

DeviceFarmテスト設定

作成済みのDeviceFarm Project及びDeviceFarm Device Poolを選択して、buildで固めたzipファイルを指定する。

スクリーンショット 2017-09-14 時刻 20.34.19.png

テスト実行

Jenkinsでテスト実行後、DeviceFarmマネジメントコンソールへのレポートリンク等がJenkinsのテスト結果画面に表示されます。

スクリーンショット 2017-09-14 時刻 20.39.01.png

その他(開発中のテストについて)

54.244.50.32~54.244.50.63. の IP範囲をホワイトリストに登録 すれば、開発中資源もテストできそうです。

終わりに

利用事例が少なそうですが、かなり使えそうなサービスなので、今後積極的に利用していきたいと思います。

続きを読む

AWSのS3サービスにMavenリポジトリを構築

始めに

Apache Mavenを利用する場合、インハウスリポジトリを構築すると便利なので、これまでWebDAVが使えるWebサーバに環境構築していました。
AWSではS3に構築することが出来るので、費用面からしても断然有利なので、今回はこれを使ってみましょう。

準備環境

準備した環境は以下の通り。
Windows7 Pro
Java 1.8
Maven 3.3.3
Eclipse4.6
Spring Boot 1.5.6.RELEASE(デモ用)

AWS作業

AWSは既存のアカウントでも良いですが、今回はセキュリティ対策のため、S3にアクセス可能なアカウントを用意しました。
アクセスポリシーは、こんな感じで良いと思います。
your-bucketのところは、各自で書き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket",
                "arn:aws:s3:::your-bucket/*"
            ]
        }
    ]
}

Java&Eclipse作業

適当なMavenプロジェクトを作成します。
私はSpring Bootプロジェクトで試しました。
以下は抜粋ですので、必要に応じて書き換えてください。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- (省略) -->

  <repositories>
    <repository>
      <id>s3-repos</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>aws-snapshot</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/snapshot</url>
    </repository>
  </distributionManagement>

  <dependencies>
    <dependency>
  <!-- (省略) -->
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- Windows環境におけるJunit実行時の文字化け対応 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <junitArtifactName>junit:junit</junitArtifactName>
          <encoding>UTF-8</encoding>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
          <argLine>-Dfile.encoding=UTF-8</argLine>
          <!-- <skipTests>true</skipTests> -->
        </configuration>
      </plugin>
    </plugins>
    <extensions>
      <extension>
        <groupId>org.springframework.build</groupId>
        <artifactId>aws-maven</artifactId>
        <version>5.0.0.RELEASE</version>
      </extension>
    </extensions>
  </build>
</project>

あとは、Mavenコマンドで使用するユーザ設定を行います。
Eclipseの場合は、ウィンドウ→設定→Maven→ユーザー設定か、Maven実行時にユーザ設定を指定します。
Maven実行時のパラメータ設定でも良いかと思います。
S3へのアクセスにプロキシ設定が必要な場合は、プロキシサーバの設定も適宜追加します。

setting.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <proxies>
    <proxy>
      <id>http_proxy</id>
      <active>true</active>
      <protocol>http</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>https_proxy</id>
      <active>true</active>
      <protocol>https</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>s3_proxy</id>
      <active>true</active>
      <protocol>s3</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
  </proxies>
  <servers>
    <server>
      <id>aws-release</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
    <server>
      <id>aws-snapshot</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
  </servers>
</settings>

実行結果

mvn deplyコマンドを実行して、S3に登録を行います。
snapshotかreleaseかは、アクセスするリポジトリ名を切り替えればOKかと思います。
他にいいやり方があるとは思いますが、とりあえずこれで。

[INFO] --- maven-deploy-plugin:2.8.2:deploy (default-deploy) @ ews ---
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar
[INFO] Configuring Proxy. Proxy Host: xx.xx.xx.xx Proxy Port: xx
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar (2000 KB at 1000.0 KB/sec)
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom (5 KB at 1.0 KB/sec)
[INFO] Downloading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/maven-metadata.xml (300 B at 0.1 KB/sec)

考察

インターネットで調べたものの、手順がよく分からなかったので、実際にやってみました。
他のプロジェクトからうまく引っ張れるかどうか試していませんが、jarファイルを一般公開したくないなぁって言う場合には、かなり使えるんじゃ無いでしょうか。

続きを読む

EC2+goofysでS3バケットをマウントしたい

概要

  • S3をファイルシステムとしてマウントしたいと思い試行錯誤したメモ。
  • 実現するためのOSSでは メジャーなもので s3fsgoofys があるっぽい。
  • goofys の方が評判が良いようなのでgoofysで検証。

目標

  • golangおよび gooyfs を導入
  • fstab で起動時に毎回マウントしたいので諸々の設定を整備

本家情報

goofysインストールの参考情報


作業環境

  • AWS + EC2(AmazonLinux) + S3

事前準備

1. マウント対象のS3バケットを新規作成。
2. 作成したバケットにアクセス可能となる IAMアカウントを用意
3. AmazonLinuxのEC2を作成し ec2-user または root でaws configure でIAMの設定を完了させておく
4. マウントポイントを先に作成しておく: 例) `mkdir -p /mnt/s3test`

参考サイトに従ってインストールを進めたが最新版goofysはそのままでは実装できなかった。とりあえずメモ残し。

バージョン確認

  • go versiongo version go1.7.5 linux/amd64 が入った

goofys インストール

  • go get github.com/kahing/goofys
  • バージョン確認: goofys --versiongoofys version 0.0.17-usemake build’ to fill version hash correctly`

マウント実行(エラー)

  • goofys <bucket> <mountpoint>

エラーメッセージ: goofys/internal/handles.go:456: undefined: url.PathUnescape
作者のkahing氏コメント: https://github.com/kahing/goofys/issues/196
go 1.8以上を入れてね、との事なのだが AmazonLinuxのリポジトリでは go 1.7.5 が入るので yumではNGか…

yumで導入したパッケージ一式を撤去

  • yum remove golang fuse -y

go lang本家からLinux用バイナリを取得して再度環境を構築

go lang のLinux用バイナリファイルを/usr/local/src へDL

  • cd /usr/local/src
  • wget https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz

go lang のバイナリファイルを展開

  • tar -C /usr/local -xzf /usr/local/src/go1.9.linux-amd64.tar.gz
  • ls -al /usr/local/go

go lang バイナリを展開した先へ path を通す

  • export GOROOT=/usr/local/go
  • export PATH=$PATH:$GOROOT/bin
  • env | grep -i go

${GOROOT} は goライブラリの設置されているパスを設定する環境変数らしい…
参考: GOROOT と GOPATH : “http://qiita.com/1000ch/items/e42e7c28cf7a7b798a02#goroot-%E3%81%A8-gopath

go lang のパスが通った事とバージョンを確認

  • go versiongo version go1.9 linux/amd64 が入った

goofysを再度インストール

  • export GOPATH=${HOME}/go
  • go get github.com/kahing/goofys
  • go install github.com/kahing/goofys

${GOPATH} は作業ディレクトリを指す環境変数らしい…
参考: “http://qiita.com/1000ch/items/e42e7c28cf7a7b798a02#goroot-%E3%81%A8-gopath

goofys 実行ファイルのあるディレクトリにもpathを通す

  • export PATH=${PATH}:${GOPATH}/bin

S3バケットをマウントしてみる

  • goofys <bucket> <mountpoint>

エラーが出る > main.FATAL Unable to mount file system, see syslog for details syslogを見てねとの事なので確認。
syslogには main.FATAL Mounting file system: Mount: mount: running fusermount: exec: "fusermount": executable file not found in $PATH#012#012stderr:#012 という事で fusemount コマンドが無い、というエラーのためマウントできないっぽい。

エラー対処

goofys本家のInstallation の項目で On Linux, install via pre-built binaries. You may also need to install fuse-utils first. って書いてた…
そういえば↑の手順の中で fuseパッケージを1回 yumで入れたものを消してたな…
fuse についてよく知らなかったので、go関連のパッケージであればgolangが関連パッケージで上書きインストールされてしまう?とも思ったがfuse パッケージ単体でOKだった
fuseとは : https://ja.wikipedia.org/wiki/Filesystem_in_Userspace
fuseとは : https://www.ibm.com/developerworks/jp/linux/library/l-fuse/index.html
libfuse公式 : https://github.com/libfuse/libfuse

goofys マウント実行

  • マウント: goofys <bucket> <mountpoint>
  • アンマウント : umount <mountpoint>

その他調整

  • DocumentRootに利用できるか
  • fstab で再起動後も設定反映させたい
  • 関連環境変数設定のまとめ
  • 再起動して確認

DocumentRootに利用できるか

  • mount <bucket> <mountpoint>/ -o allow_other,--uid=<uid>,--gid=<gid>
  • uid gid の確認 : id apache または id nginx などで

allow_otherオプション無しでマウントしていると apache/nginx の実行ユーザーから読めないっぽい…
allow_otherオプション+ uid gid はdaemon実行ユーザと同じ設定にしてマウントすれば動いた:

fstab で再起動後も設定反映させたい

  • /etc/fstab に設定を追記
  • <fullpath/to/>goofys#<bucket> <mountpoint> fuse _netdev,allow_other,--dir-mode=0755,--file-mode=0666,--uid=<uid>,--gid=<gid> 0 0

本家のUsage にfstab で使う場合は root にAWS credentials 設定をせよとあるのでrootのユーザ環境に設定。
fstab のサンプルもUsageにある : goofys#bucket /mnt/mountpoint fuse _netdev,allow_other,--file-mode=0666 0 0
その他参考: http://caters.works/2016/08/%E3%81%95%E3%81%8F%E3%82%89%E3%81%AEvps%E3%81%AB-goofys-%E3%81%A7-s3-%E3%82%92%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88/

fstab 設定の試行(再起動無しで)

  • sudo mount -a

fstabに記載したのと同じ<mountpoint>をマウントした状態で実行している場合は、umountした後に実行

aws credential の profile を指定したい場合(未検証)

  • goofys#<bucket> <mountpoint> fuse _netdev,allow_other,--file-mode=<filemode>,--profile=<profile> 0 0 っぽい

本家のissues でやり取りを発見 : https://github.com/kahing/goofys/issues/222

環境変数回りをまとめる

  • rootユーザの環境変数へ設定を追加 : /root/.bashrc
export GOROOT=/usr/local/go
export GOPATH=${HOME}/go
export PATH=${PATH}:${GOROOT}/bin:${GOPATH}/bin

システム再起動後にも各所の設定が反映される事を確認

環境変数 : sudo env | grep -i go
go lang : go version
goofys : goofys -v
fstabログ : sudo tail -f /var/log/messages | grep goofys : main.INFO File system has been successfully mounted.
マウント状況 : df -ah # s3の空き表示って 1ペタになるのか…

続きを読む

Three.jsのかっこいいサンプルとAWSを連携させてみた

かっこいい画面が作りたかったので、やってみた備忘録。

やりたいこと

Three.jsがとてもかっこいいんですが、値の設定が静的なので、何とか動的に値を変えつつ画面に反映できないかな、、、と。このサンプルを使ってみます。

スクリーンショット 2017-05-14 19.08.58.png

イメージとしては、この画像で異常が検出されたところを赤くアラート表示したりできないかな、と。

実際の流れの整理

Githubからサンプルを持って来て、必要なものだけ抜き出します。

異常があったら表示する、という感じにしたいので、以前投稿したロジックを使います。

AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)

最新値を取得する中で、異常があったら対象の項目に変化をつけたいと思います。

とりあえずサンプルをAWS上で動かす

AWS上で動作させたいので、とりあえずこのサンプルをAWS上に乗せる必要があります。

  1. 必要なファイルの抽出(抽出結果などはこちらに。https://github.com/kojiisd/aws-threejs
  2. 適当な名前のバケットをS3に作成(今回は「aws-three」という名前にしました)
  3. ファイルのアップロード
  4. パーミッションの適用

パーミッションの適用は、とりあえず以下のコマンドで一括でつけました。実際の運用を考えると、もっと慎重にならないといけないところだとは思います。こちらのサイトを参考にさせてもらいました。

$ aws s3 ls --recursive s3://aws-three/ | awk '{print $4}' | xargs -I{} aws s3api put-object-acl --acl public-read --bucket aws-three --key "{}"

これで一旦サンプルがS3上で動くようになりました。

データの中身を理解する

実際にソースを読んで見ると、一つ一つのオブジェクトをどの様に格納しているかがわかります。

var table = [
    "H", "Hydrogen", "1.00794", 1, 1,
    "He", "Helium", "4.002602", 18, 1,
    "Li", "Lithium", "6.941", 1, 2,
    "Be", "Beryllium", "9.012182", 2, 2,
    :
    :

5つの要素で1セットとしてオブジェクトを表示している様です。元の実装がいいかどうかは置いておいて(^^;とりあえずこれを動的に変更できるようにします。

DBからの値をtableに反映する

データの構造自体はとても単純なので、1レコード5カラムのデータを持つテーブルを作成すれば、後々データの内容全てをDBから持ってくることも可能になりそうです。

DynamoDBで下記の様な単純なテーブルを作成します。もちろんAWS Consoleから作成可能ですが、Localで同じスキーマを作成したい場合はこんな感じで定義を流し込めばOK。

var params = {
    TableName: "test-three",
    KeySchema: [
        {
            AttributeName: "id",
            KeyType: "HASH"
        },
        {
            AttributeName: "timestamp",
            KeyType: "RANGE"
        }
    ],
    AttributeDefinitions: [
        {
            AttributeName: "id",
            AttributeType: "S"
        },
        {
            AttributeName: "timestamp",
            AttributeType: "S"
        }
    ],
    ProvisionedThroughput: {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1
    }
};

dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err);
    else ppJson(data);
});

はい、無事出来ました。

スクリーンショット 2017-07-09 16.12.16.png

データの準備と投入

次にデータを流し込みます。こんな感じでpythonスクリプトでサクッと。実行する前に、クライアントのAWS認証設定が正しいことを確認してください。

data-insert.py
args = sys.argv

if len(args) < 4:
    print "Usage: python data-insert.py <FileName> <Region> <Table>"
    sys.exit()

dynamodb = boto3.resource('dynamodb', region_name=args[2])
table = dynamodb.Table(args[3])

if __name__ == "__main__":
    print "Data insert start."
    target = pandas.read_csv(args[1])
    for rowIndex, row in target.iterrows():
        itemDict = {}
        for col in target:
            if row[col] != None and type(row[col]) == float and math.isnan(float(row[col])) and row[col] != float('nan'):
                continue
            elif row[col] == float('inf') or row[col] == float('-inf'):
                continue
            elif type(row[col]) == float:
                itemDict[col] = Decimal(str(row[col]))
            else:
                itemDict[col] = row[col]
        print itemDict

        table.put_item(Item=itemDict)

    print "Data insert finish."

とりあえずこんなデータを用意しました。先ほどのスクリプトで投入します。

id,score,timestamp
"H",0,"2017-07-23T16:00:00"
"H",0,"2017-07-23T16:01:00"
"H",0,"2017-07-23T16:02:00"
"H",0,"2017-07-23T16:03:00"
"H",0,"2017-07-23T16:04:00"
"H",1,"2017-07-23T16:05:00"
"H",0,"2017-07-23T16:06:00"

無事投入できました。

$ python data-insert.py sample-data.csv us-east-1 test-three
Data insert start.
{'timestamp': '2017-07-23T16:00:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:01:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:02:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:03:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:04:00', 'score': 0, 'id': 'H'}
{'timestamp': '2017-07-23T16:05:00', 'score': 1, 'id': 'H'}
{'timestamp': '2017-07-23T16:06:00', 'score': 0, 'id': 'H'}
Data insert finish.

スクリーンショット 2017-07-23 17.33.30.png

API Gatewayの設定

絞り込み結果を得るためのLambdaを実行するRESTの口を設けます。API Gatewayを設定してPOST通信でデータが来た場合に該当のLambdaを実行するようにします。

スクリーンショット 2017-07-23 18.13.07_dec.png

画面からDynamoDBのデータを取得する

定期的に画面からDynamoDBのデータを取得します。今回はCognitoによるセキュアな通信は実装しません。他のページで色々と実装されている方がいるので、そちらを参考にして見てください。

処理の流れとしては、以下のような感じ。(S3からの取得処理が定期的に実行される)

S3 → API Gateway → Lambda → DynamoDB

API GatewayでSDK生成

API Gatewayの設定を以下にし、SDKを生成します。

  1. HTTPメソッドはPOSTを指定
  2. とりあえず認証などは今回はかけない

で、作成されたSDKを、本家のページを参考に埋め込みます。

API Gateway で生成した JavaScript SDK を使用する

以下のコードをScriptタグ部分に貼り付けました。

        <script type="text/javascript" src="../extralib/axios/dist/axios.standalone.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/rollups/hmac-sha256.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/rollups/sha256.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/components/hmac.js"></script>
        <script type="text/javascript" src="../extralib/CryptoJS/components/enc-base64.js"></script>
        <script type="text/javascript" src="../extralib/url-template/url-template.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/sigV4Client.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/apiGatewayClient.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/simpleHttpClient.js"></script>
        <script type="text/javascript" src="../extralib/apiGatewayCore/utils.js"></script>
        <script type="text/javascript" src="../extralib/apigClient.js"></script>

実際にAPI Gatewayにアクセスするスクリプトはこんな感じになります。

SDKを用いてAPI Gatewayにブラウザからアクセスしてみる

var apigClient = apigClientFactory.newClient();

var params = {
    // This is where any modeled request parameters should be added.
    // The key is the parameter name, as it is defined in the API in API Gateway.
    "Content-Type": "application/x-www-form-urlencoded"
};

var body = {

    "label_id": "id",
    "label_range": "timestamp",
    "id": [
    "H"
    ],
    "aggregator": "latest",
    "time_from": "2017-07-23T16:00:00.000",
    "time_to": "2017-07-23T16:06:00.000",
    "params": {
    "range": "timestamp"
    }
};


  var additionalParams = {
    // If there are any unmodeled query parameters or headers that must be
    //   sent with the request, add them here.
    headers: {
    },
    queryParams: {
    }
  };

  apigClient.rootPost(params, body, additionalParams)
      .then(function(result){
        // Add success callback code here.
        alert("Success");
        alert(JSON.stringify(result));
      }).catch( function(result){
        // Add error callback code here.
      });

パラメータ(body部)には「AWS LambdaでDynamoDBから取得した値に任意の集計をかける(グルーピング処理追加)」で必要になるJSONを渡します。

API GatewayにアクセスするにはCORSの設定が必要なので、AWS Consoleから設定します。この際Resourcesで変更したものはStaging環境に再デプロイをしないと追加で設定した項目は反映されないので注意(ハマった。。。)

「Enable CORS」から設定できます。

スクリーンショット 2017-09-10 10.56.00.png

実際に画面にアクセスすると、サーバから値が取れたことがわかります。(今回はAPI KEYなどはOFFにしていますが、実際に運用するとなったら、もちろん考慮が必要です。)

スクリーンショット 2017-09-10 11.03.04.png

スクリーンショット 2017-09-10 11.03.11_deco.png

無事値が取れました。ここまでくればもう一息です。

取得した値を元に画面に変更を加える

さて、先ほど取得できた値には「score」というものが入っています。この「score」値を元に画面のコンポーネントに変化を加えます。

持ってきたサンプルコードの中で色に影響を与えているのは以下のコードになります。

for ( var i = 0; i < table.length; i += 5 ) {

    var element = document.createElement( 'div' );
    element.className = 'element';
    element.style.backgroundColor = 'rgba(0,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')';

全てのコンポーネントに対してrgbaで値を与えているので、ここを変更します。とはいえ、そのまま変更しようとしてもinit()メソッドを実行すると追加でコンポーネントが描画されてしまうので、描画後に変更が可能なように以下のようなidの埋め込みを行います。

var element = document.createElement( 'div' );
element.className = 'element';
element.id = table[ i ];                        // ここが追加したところ
element.style.backgroundColor = 'rgba(0,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')';

これらの準備を前提として、処理の流れは以下とします。

  1. DynamoDBから取ってきたjson値をオブジェクトに変換。
  2. 一旦オブジェクト配列としてidとscore値を持たせるようにする。
  3. オブジェクト配列でループ処理を実装し、その中でscore値が「0」ではないidに対して、色の変更を実施する。

今回の準備を踏まえると、上記で対象となるのは「id: H」となります。DynamoDBからデータを取得するために、rootPost呼び出し後のコールバック処理を以下のように変更します。今回色変更の処理をシュッと実装するために、jQueryを読み込ませています。

apigClient.rootPost(params, body, additionalParams)
    .then(function(result){
        var resultObjArray= new Array();
        // Add success callback code here.
        var resultJson = JSON.parse(result.data);
        for (var index = 0; index < resultJson.length; index++) {
            var resultObj = new Object();
            resultObj.id = resultJson[index].id;
            resultObj.score = resultJson[index].score
            resultObjArray[index] = resultObj;
        }

        for (var index = 0; index < resultObjArray.length; index++) {
            var resultObj = resultObjArray[index];
            if (resultObj.score != 0) {
                $('#' + resultObj.id).css('background-color', 'rgba(255,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')')
            }
        }

    }).catch( function(result){
        // Add error callback code here.
        alert("Failed");
        alert(JSON.stringify(result));
    });

DynamoDBに格納されている値(検索期間の最新)を「1」に変更して画面を更新すると、無事赤くなりました。

スクリーンショット 2017-09-10 18.58.41.png

実行タイミングを任意にするためにボタンからのクリックイベントとして実装する

このままでもいいのですが、今のままだと最初のアニメーションが終わる前に色が変わるので、色が変わった感がありません。ですので一つボタンを追加してクリックしたら画面に値を反映する、という手法を取ろうと思います。

先ほどのrootPostを呼び出すプログラムを、画面に配置したボタンのイベントとして処理させます。

<button id="getData">Get Data from DynamoDB</button>

画面にはこんな感じでボタンを配置し、jQueryでイベントを登録します。

$(document).ready(function(){
    $("#getData").click(function() {
        apigClient.rootPost(params, body, additionalParams)
        .then(function(result){
            var resultObjArray= new Array();
            // Add success callback code here.
            var resultJson = JSON.parse(result.data);
            for (var index = 0; index < resultJson.length; index++) {
                var resultObj = new Object();
                resultObj.id = resultJson[index].id;
                resultObj.score = resultJson[index].score
                resultObjArray[index] = resultObj;
            }

            for (var index = 0; index < resultObjArray.length; index++) {
                var resultObj = resultObjArray[index];
                if (resultObj.score != 0) {
                    $('#' + resultObj.id).css('background-color', 'rgba(255,127,127,' + ( Math.random() * 0.5 + 0.25 ) + ')')
                }
            }

        }).catch( function(result){
            // Add error callback code here.
            alert("Failed");
            alert(JSON.stringify(result));
        });
    });
});

これで無事画面から操作することが可能になりました。ボタンを押すといい感じに「H」の要素が赤く光っています。

スクリーンショット 2017-09-10 23.08.28.png

まとめ

もともとかっこいいと思っていたThree.jsの画面をカスタマイズして監視画面(IoTとかのデバイス監視画面)のような機能にできないものか、と実装してみましたが、なんとかここまでたどり着けました。

DynamoDBからの取得間隔やIDの指定方法など、まだまだハードコーディングな部分はありますが、土台は出来上がったのであとはちょっとのカスタマイズで実際に使えそうなところまではイメージが持てました。元々の実装の仕組みも理解できたので、条件が合えば文言を変えるなども実現できそうです。

やっぱり業務で使うものもかっこいい画面でないとね、と思うこの頃です。
(GithubのコードはAPI GatewayのSDKさえ埋め込めば動くような作りにしています)

https://github.com/kojiisd/aws-threejs

続きを読む

CloudFrontを利用して独自ドメインのGitHub PagesをHTTPS化する

tl;dr

方法として2つあるが、設定がシンプルであることと、エッジ-オリジン間もHTTPS化できるため後者がオススメ。

  1. CNAME を有効のままで、CloudFrontで Host ヘッダを転送する設定とする
  2. CNAME を無効にして、 https://example.github.io/reponame/ でアクセスするようにし、/reponame/ をOriginのパスとして利用する

Zone Apex (サブドメイン無し) の場合でも、どちらでも可能。

共通

以下の例で進めていきます。

  • Default Page Domain: username.github.io
  • Repository Name: reponame
  • CNAME: reponame.example.jp

  • SSL証明書は、事前にACMで用意しておいてください。

    • ACMで証明書を作成する際は us-east-1 (N.Virginia) リージョンで作る必要があるのでお忘れなく。
  • また、Route53でDNSを管理していることを前提として説明しますが、別のDNSサービスでも手順は変わるものの実現可能です。

  • 切り替えるレコードのTTLは切り替えがなる早で反映されるように一時的に短くしておいてください

CNAMEを有効のまま設定する

GitHub Pages 側をCNAMEを有効にしたままでCloudFrontを設定し切り変えます。

http://reponame.example.jp で GitHub Pages が開ける状態となっている前提で進めていきます。

  1. CloudFrontディストリビューションを Host ヘッダを転送する設定で作成する
  2. Route53のレコードをCloudFrontに向ける

CloudFrontディストリビューションの作成

Origin Domain Name を username.github.io

cf1.png

Behavior Settingsでは、

  • Redirect HTTP to HTTPS で HTTP でアクセスしたときに HTTPS にリダイレクトするようにする
  • Forward HeadersWhitelist を設定し、Host を追加する
  • Compress Object Automatically はお好みで。静的テキストコンテンツが多くなる傾向があると思うので有用だと思います

cf2.png

そして、Distribution Setting では、予めACMに作成しておいたCNAMEにマッチする証明書を設定します。

cf3.png

これでディストリビューションを作成し、利用可能になるまでしばし待ちます。
作成したDistributionのドメイン名で、 https://dxxxxxxxxxxxxx.cloudfront.net という形でアクセスできるようになります。

動作確認

ドメインの名前解決先を切り替えてしまうと、問題があった場合に困ったことになるので動作確認します。

Hostsで確認

先程作成したDistributionのドメイン名 (dxxxxxxxxxxxxx.cloudfront.netの形式) をnslookupします。

nslookup dxxxxxxxxxxxxx.cloudfront.net
Server:     192.168.179.1
Address:    192.168.179.1#53

Non-authoritative answer:
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.52
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.163
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.25
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.78
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.12
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.186
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.102
Name:   dxxxxxxxxxxxxx.cloudfront.net
Address: 13.32.230.208

1つ適当に選んで、Hostsに書きます。

reponame.example.jp    13.32.230.52

ブラウザを開いて http://reponame.example.jp にアクセスし、HTTPSにリダイレクトされること、HTTPSで正常にアクセスできることを確認します。
CloudFrontのIPは逐次変わるため、永続的にこのIPで動作確認できるわけではないことにご注意ください。
動作確認が終わったら確実にHostsエントリを削除しましょう。

curlで確認

--connect-to が便利です!

  • -v: デバッグ表示
  • --silent: プログレスバーを表示しない
  • --location: リダイレクトに従う
  • --connect-to:HOST1:PORT1:HOST2:PORT2: HOST1:PORT1 へのリクエストを HOST2:PORT2 に送る。この例では、reponame.example.jp へのリクエストを dxxxxxxxxxxxxx.cloudfront.net に送る。

リダイレクトして、HTTPSでレスポンスがCloudFrontから返却されていることを確認する。

curl -v --silent --location --connect-to "reponame.example.jp::dxxxxxxxxxxxxx.cloudfront.net:" http://reponame.example.jp/ > /dev/null
* Connecting to hostname: dxxxxxxxxxxxxx.cloudfront.net
*   Trying 13.32.230.25...
* TCP_NODELAY set
* Connected to dxxxxxxxxxxxxx.cloudfront.net (13.32.230.25) port 80 (#0)
> GET / HTTP/1.1
> Host: reponame.example.jp
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Server: CloudFront
< Date: Sun, 10 Sep 2017 10:06:49 GMT
< Content-Type: text/html
< Content-Length: 183
< Connection: keep-alive
< Location: https://reponame.example.jp/
< X-Cache: Redirect from cloudfront
< Via: 1.1 78dc5acc7fb7b026e9215d8188becd98.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: ymg3bDg7MThc2VVOuIpOC3r42GyuRDXJUS0HnB9cKnWWTnRjWD-e_g==
<
* Ignoring the response-body
{ [183 bytes data]
* Connection #0 to host dxxxxxxxxxxxxx.cloudfront.net left intact
* Issue another request to this URL: 'https://reponame.example.jp/'
* Connecting to hostname: dxxxxxxxxxxxxx.cloudfront.net
*   Trying 13.32.230.25...
* TCP_NODELAY set
* Connected to dxxxxxxxxxxxxx.cloudfront.net (13.32.230.25) port 443 (#1)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: reponame.exmple.jp
* Server certificate: Amazon
* Server certificate: Amazon Root CA 1
* Server certificate: Starfield Services Root Certificate Authority - G2
> GET / HTTP/1.1
> Host: reponame.example.jp
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Content-Length: 18280
< Connection: keep-alive
< Server: GitHub.com
< Last-Modified: Sat, 09 Sep 2017 13:30:08 GMT
< Access-Control-Allow-Origin: *
< Expires: Sun, 10 Sep 2017 10:04:44 GMT
< Cache-Control: max-age=600
< X-GitHub-Request-Id: F24E:8856:173BB3:19374E:59B50BE4
< Accept-Ranges: bytes
< Date: Sun, 10 Sep 2017 09:54:44 GMT
< Via: 1.1 varnish, 1.1 45b893b1fe0ecafde6bfa63bc4abc859.cloudfront.net (CloudFront)
< X-Served-By: cache-nrt6125-NRT
< X-Cache-Hits: 0
< X-Timer: S1505037284.497836,VS0,VE232
< X-Fastly-Request-ID: 27614a7c60d3bb6edab88b25f86c9a21cc4b3899
< Vary: Accept-Encoding
< Age: 92
< X-Cache: Hit from cloudfront
< X-Amz-Cf-Id: My_BZNmrPJFeHIZsTST_5gc5A5jvk7XXVSdErVcAxhvxtfUAVud4PQ==
<
{ [15630 bytes data]
* Connection #1 to host dxxxxxxxxxxxxx.cloudfront.net left intact

Route53の変更

example.jp のHosted Zoneを開いて、reponame.example.jp のエントリを更新します。

r53.png

また、AAAAレコードも同様に設定することで、IPv6対応になります。

CNAMEを無効にするパターン

こちらはGitHub PagesでCNAMEが有効になっているのを無効にする方法です。
停止期間が無いように、一時的にリポジトリをコピーし、また、別ドメインでテストをできるようにします。

  1. GitHubのリポジトリを一時的に作成した別のリポジトリにコピーし、GitHub Pagesを表示できるようにする
  2. CloudFrontディストリビューションを作成
  3. Route53でテストドメインの設定
  4. テストドメインで動作確認
  5. Route53で本番の切り替え

リポジトリのコピー

やり方はいろいろありますが、一例。

  1. 対象のリポジトリ (reponame) をローカルにcloneする
  2. GitHubで新しいリポジトリを作る (reponame-https-tmp)
  3. リモートを追加する
    • git add remote https git@github.com:username/reponame-https-tmp.git
  4. 適宜ブランチを変更する (gh-pages を表示する設定であれば、gh-pages に変更)
  5. 新しいリポジトリにpushする
    • git push --set-upstream https gh-pages (gh-pages の場合)
  6. CNAME ファイルが存在する場合は、新しいリポジトリ側で削除する

Settings で GitHub Pages が表示されるよう設定し、https://username.github.io/reponame-https-tmp/
で表示できることを確認する。
(リンクや埋め込んだ画像等が /hoge とか絶対パスになって、うまく表示できないかもしれないですが、それらは一旦スルー)

CloudFrontディストリビューションの作成

まず、新しく作成したリポジトリに向けたディストリビューションを作成します。

Origin Settings では

  • Origin Domain Name を username.github.io
  • Origin Path を 、/reponame-https-tmp/
  • Origin Protocol Policy は HTTPS Only にします

cf4.png

Behavior Settingsでは、

  • Redirect HTTP to HTTPS で HTTP でアクセスしたときに HTTPS にリダイレクトするようにする
  • Compress Object Automatically はお好みで。静的テキストコンテンツが多くなる傾向があると思うので有用だと思います

cf5.png

そして、Distribution Setting では、CNAMEs に切り替え予定の reponame.example.jp とは別に動作確認用の reponame-test.example.jp といったCNAMEも付与します。
そして、ACMに作成しておいたCNAMEにマッチする証明書を設定しますが、この証明書は *.example.jp のワイルドカードかAdditonal Namesを使って両方をカバーしたものを用意、適用します。

cf6.png

これでディストリビューションを作成し、利用可能になるまでしばし待ちます。

Route53でテストドメインの設定

  • Route53で先ほど作成した動作確認用のアドレス (reponame-test.example.jp) からCloudFrontディストリビューションへのエイリアスを作成します。

(再掲)
r53.png

動作確認

http://reponame-test.example.jp/ がHTTPSにリダイレクトされること、https://reponame-test.example.jp/ が表示されることを確認する。

wgetでクロールさせて、デッドリンクや、github.io 側に移動してしまったりが無いかを確認するのもよいです。

wget --spider --recursive --no-directories --no-verbose https://reponame-test.example.jp/

Route53で本番の切り替え

続いて、一時的に https://reponame.example.jp を、 reponame-https-tmp 側を表示するよう切り替えます。

  • Route53で本番のドメイン (reponame.example.jp) から作成したCloudFrontディストリビューションへのエイリアスを作成します。

元リポジトリの設定変更

reponame.example.jp がCloudFrontに向くようになって、切り替える前のレコードのTTLが十分経過してから、元リポジトリの設定を変更し、https://username.github.io/reponame/ でアクセスできるようにします。

  • CNAME ファイルがあれば削除する
  • Settings で独自ドメインの設定を無効にする

CloudFrontのオリジンの追加、設定変更

  1. OriginsタブのCreate Originから https://username.github.io/reponame/ を指すオリジンを新しく作成する。
  2. Behaviorsタブで、デフォルトのBehaviorにチェックを入れて、Editをクリックし、Origin を新しく作った本番 (reponame) に向いたものに変更し、”Yes, Edit” で反映する
  3. 設定が反映されるのを待ち、その後、変わらずにページが表示できることを確認する
  4. 最初に作ったオリジンを削除する

後片付け

  • Route53のテストドメインを削除する
  • 一時的に作ったリポジトリを削除する

備考

GitHub Pages 以外のサイトにも応用可能ですが、CNAMEが正しく設定しているか定期的に確認し、設定されていないと独自ドメイン設定を解除するサービスでは、CNAME設定のまま切り替えをするパターンだと後々表示されなくなるので注意です。
設定画面でCNAMEが正しく設定されているか確認してくれるサイトは、この可能性が高いです。

また、独自ドメインでない場合にページ内のリンクのURLが変わってしまうようなサービスでは、上記「CNAMEを無効にして切り替える」は難しい認識です。

続きを読む

Ubuntu Server 16.04 LTS(HVM) 起動のCloudFormationテンプレート

EC2起動の時に選択肢に表示されるUbuntu起動のテンプレートです.
CloudFormationのメタデータ取得や変更検知、状態通知などを行うヘルパースクリプトのインストールに手間取ったのであげておきます.
pipでインストールしてるので長くなっちゃってますが、wgetでファイル取って来る方法も取れそうです.

参考:

AWSTemplateFormatVersion: '2010-09-09'
Description: CFn template for ubuntu
Parameters:
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t2.nano
    AllowedValues:
      - t2.nano 
    ConstraintDescription: must be a valid EC2 instance type.
  KeyName: 
    Description: Name of an existing Amazon EC2 key pair for SSH access
    Type: AWS::EC2::KeyPair::KeyName
  SSHLocation:
    Description: The IP address range that can be used to SSH to the EC2 instances
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.  
Mappings:
  AWSRegion2AMI:
    ap-northeast-1: 
      ubuntu: ami-ea4eae8c
Resources:
  ServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Simple example for cfn-init
      AWS::CloudFormation::Init:
        config:
          packages:
            apt:
              httpd: [] 
          files:
            /etc/cfn/cfn-hup.conf:  # CFn側の変更検知のための設定
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:  # 自動で更新を実行するための設定
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.DeepLeargingServerInstance.Metadata.AWS::CloudFormation::Init
                action=/usr/local/bin/cfn-init -s ${AWS::StackId} -r ServerInstance --region ${AWS::Region}
                runas=root
          services:
            sysvinit:
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegion2AMI, !Ref 'AWS::Region', ubuntu]      
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'ServerSecurityGroup'
      KeyName: !Ref 'KeyName'
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe
          apt-get update
          # python 2.7, pipインストール
          apt-get -y install build-essential
          apt-get -y install python-dev python-pip
          # aws-cloudformation-bootstrapのインストール
          pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
          # cfn-hupの設定
          cp -a /usr/local/init/ubuntu/cfn-hup /etc/init.d/cfn-hup
          chmod u+x /etc/init.d/cfn-hup
          update-rc.d cfn-hup defaults  # シンボリックリンク作成
          service cfn-hup start
          # メタデータからのファイルとパッケージのロード
          /usr/local/bin/cfn-init -v --stack ${AWS::StackName} --resource ServerInstance --region ${AWS::Region}
          # cfn-init から取得したの状態の通知
          /usr/local/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ServerInstance --region ${AWS::Region}
    CreationPolicy:
      ResourceSignal:
        Timeout: PT10M
  ServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 22
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref 'SSHLocation'
Outputs:
  WebsiteURL:
    Description: Application URL(Not Work, Just Sample)
    Value: !Sub 'http://${ServerInstance.PublicDnsName}'

続きを読む

忙しい人のための Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities メモ

こんにちは、ひろかずです。

2017年7月に「Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities」が公開されました。
日本語翻訳されていないので、なかなか読めていない方もいると思います。
リリースから少し時間が経ってしまいましたが、読了したので一筆書きます。

どんなドキュメントか

OWASP Top10 で上っている各項目についての解説と、各項目についてAWS WAFでどのような対応ができるかを記載したもの。
Conditionを作る上でのアドバイスも記載されている。

参照ドキュメント

Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities

A1 – Injection

SQLインジェクション攻撃は、比較的簡単に検出できるが、バックエンドの構造によって複雑さが変わるため、アプリケーション側での対応も必要。
cookieやカスタムヘッダをデータベース参照に使う場合は、Conditionで検査対象とするように設定すること。
リクエスト中にクエリを流す事を是としているサービスの場合、対象URLを検知除外(バイパス)するような設定を検討すること。

A2 – Broken Authentication and Session Management

String Matchを活用して、盗まれた(怪しい)トークンを「安全のために」ブラックリストに登録する
デバイスのロケーションが異なるトークンを検出する等、ログから悪性利用をしているトークンを分析して、ブラックリストに登録するような使い方。
Rate-basedルールを用いて、認証URLを総当り攻撃を防ぐ施策も有効。

A3 – Cross-Site Scripting (XSS)

XSS攻撃は、HTTPリクエストで特定のキーHTMLタグ名を必要とするため、一般的なシナリオで比較的簡単に軽減できる。
BODYやQUERY_STRING, HEADER: Cookieを検査するのが推奨される。
URLを検査対象とすることは一般的ではないが、アプリケーションが短縮URL使っていると、パラメータはURLパスセグメントの一部として表示され、クエリ文字列には表示されない(後でサーバー側で書き換えられる)
CMSエディタ等、たくさんHTMLを生成するサービスには効果は薄い。
対象URLを検知除外(バイパス)する場合は、別途対策を検討すること。
加えて、<script>タグを多用するイメージ(svgグラフィックフォーマット)やカスタムデータも誤検知率が高いので調整が必要。

A4 – Broken Access Control

2017年から新たに登場
認証ユーザーの制限が適切に動作せず、必要以上に内部アプリケーションオブジェクト(不正なデータの公開、内部Webアプリケーション状態の操作、パストラバーサル、リモート/ローカルファイルの呼び出し)を操作できるというもの。
機能レベルのアクセス制御の欠陥は、一般的に後で追加されたアプリケーションで発生する。
アクセスレベルは、呼び出し時に一度検証されるが、その後に呼び出される様々なサブルーチンについては、都度検証されない。
呼び出し元のコードが、ユーザーの代わりに他のモジュールやコンポーネントを呼び出す暗黙的な信頼ができてしまう。
アプリケーションが、アクセスレベルやサブスクリプションレベルに応じてコンポーネントへアクセスさせる場合は、呼び出しの都度、検証する必要がある。

AWS WAFとしては、「../」や「://」をQUERY_STRINGやURIに含むかを検証することで、リモート/ローカルファイルの呼び出しを検出できる。
これも、リクエスト中に「../」や「://」を含む事を是とするアプリケーションには効果は薄い。
管理モジュール、コンポーネント、プラグイン、または関数へのアクセスが既知の特権ユーザーのセットに限定されている場合、Byte_MatchとIPSetの組み合わせでアクセスを制限することができる。

認証要求がHTTPリクエストの一部として送信され、JWTトークン(のようなもの)でカプセル化されている場合

  • Lambda@Edgeファンクションを用いて、関連リクエストパラメータがトークン内のアサーションおよび承認と一致することを確認することで、権限不適合リクエストはバックエンドに届く前に拒否することができる。

A5 – Security Misconfiguration

セキュリティの影響を受けるパラメータの設定ミスは、アプリケーションだけではなく、OSやミドルウェア、フレームワーク全てにおいて発生し得る。
セキュリティの影響を受けるパラメータの設定ミス例

  • ApacheのServerTokens Full(デフォルト)構成
  • 本番Webサーバーでデフォルトのディレクトリ一覧を有効にしたまま
  • エラーのスタックトレースをユーザーに戻すアプリケーション
  • PHPの脆弱なバージョンと組み合わせて、HTTPリクエストを介して内部サーバ変数を上書き

AWS WAFとしては、パラメータの設定ミスを悪用するHTTPリクエストパターンが認識可能な場合に限り対応可能。

  • A4 – Broken Access Controlと同様に、特定パスやコンポーネントに対してByte_MatchとIPSetの組み合わせでアクセスを制限を行う。(Wordpress管理画面 等)
  • 脆弱なPHPを利用している場合、QUERY_STRING中の“_SERVER[“を遮断する。

その他に考慮できること

  • Amazon Inspectorでの設定ミスの捜査(rootログイン許可や脆弱なミドルウェアバージョン、脆弱性の対応状況)
  • Amazon InspectorでのCISベンチマークとの適合性チェック
  • AWS ConfigやAmazon EC2 Systems Managerを用いた、システム構成変更の検出

A6 – Sensitive Data Exposure

アプリケーションの欠陥による機密情報の暴露をWAFで緩和するのは難しい。
この欠陥には、一般に不完全に実装された暗号化が含まれる(弱い暗号の利用を許容していること)

AWS WAFとしては、HTTPリクエスト中の機密情報を識別するパターンをString-Match Conditionで検出することで、機密情報へのアクセスを検出することができる。(ホストするアプリケーションに対する深い知識が必要)
アプリケーションが、アップロードを許容する場合、Base64を示す文字列の一部を検出するString-Match Conditionが有効(BodyはBase64でエンコードされているから)
一般的ではない手法

その他に考慮できること

  • AWS内の機能を使って、接続ポイントにて、ELBで強い暗号化suiteを使用するように制限する
  • クラシックロードバランサの場合、事前定義・カスタムセキュリティポリシーで制御できる。
  • アプリケーションロードバランサの場合、事前定義セキュリティポリシーで制御できる。
  • Cloud Frontでも利用するSSLプロトコルを制限できる。

A7 – Insufficient Attack Protection

2017年から新設
このカテゴリは、新しく発見された攻撃経路や異常なリクエストパターン、または発見されたアプリケーションの欠陥にタイムリーに対応する能力に重点を置いている。
幅広い攻撃ベクトルが含まれるので、他のカテゴリと重複する部分もある。

確認ポイント

  • アプリケーションに対して、異常なリクエストパターンや大量通信を検出できるか?
  • その検出を自動化できる仕組みはあるか?
  • 不要な通信に対して、ブロックできる仕組みはあるか?
  • 悪意のあるユーザーの攻撃開始を検出できるか?
  • アプリケーションの脆弱性に対するパッチ適用までの時間はどの程度かかる?
  • パッチ適用後の有効性を確認する仕組みはあるか?

AWS WAFで何ができるか

  • Size-Content Conditionを使うことで、アプリケーションで利用するサイズ以上の通信を検出・遮断できる。
  • URIやクエリ文字列のサイズを、アプリケーションにとって意味のある値に制限すること。
  • RESTful API用のAPIキーなどの特定のヘッダーが必要になるようにすることもできる。
  • 特定IPアドレスからの5分間隔での要求レートに対する閾値設定。(例えば、UserAgentとの組み合わせでのカウントも可能)
  • Web ACLはプログラマブルで反映が早いのが利点(CloudFrontに対しても約1分で反映)
  • ログの出力傾向から分析して、Web ACLを自動調整する機能も構築できる。

AWS WAFのセキュリティオートメーション機能の活用

  • Lambdaを使って、4xxエラーを多く出しているIPアドレスを特定してIPブラックリストに登録する。
  • 既知の攻撃者に対しては、外部ソースのIPブラックリストを活用する。
  • ボットとスクレイバに対しては、robots.txtファイルの ‘disallow’セクションにリストされている特定URLへのアクセスを行ったIPを、IPブラックリストに登録する。(ハニーポットと表現されています)

A8 – Cross-Site Request Forgery

CSRFは、WebアプリケーションのStateを変更する機能を対象としている。
State変更に係るURLやフォーム送信などのHTTPリクエストを考慮する。
「ユーザーがその行動を取っている」ということを必ず証明する機構がなければ、悪意のあるユーザーによるリクエスト偽造を判断する方法はない。

  • セッショントークンや送信元IPは偽造できるので、クライアント側の属性に頼るのは有効ではない。
  • CSRFは、特定のアクションのすべての詳細が予測可能であるという事実(フォームフィールド、クエリ文字列パラメータ)を利用する。
  • 攻撃は、クロスサイトスクリプティングやファイルインクルージョンなどの他の脆弱性を利用する方法で実行される。

CSRF攻撃への対処

  • アクションをトリガーするHTTPリクエストに予測困難なトークンを含める
  • ユーザー認証プロンプトの要求
  • アクション要求時のキャプチャの提示

AWS WAFで何ができるか

  • 固有トークンの存在確認
  • 例えば、UUIDv4を活用して、x-csrf-tokenという名前のカスタムHTTPヘッダーの値を期待する場合は、Byte-Size Conditionが使える。
  • ブロックルールには、POST HTTPリクエスト条件に加える

その他にできること

  • 上記のようなルールでは、古い/無効な/盗まれた トークンの検出については、アプリケーションでの対応が必要
  • 例えばの仕組み
  • サーバが、一意のトークンを隠しフィールドとしてブラウザに送信し、ユーザーのフォーム送信時の期待値にする。
  • 期待したトークンが含まれないPOSTリクエストを安全に破棄できる。
  • 処理後のセッションストアからトークンは削除しておくことで、再利用がされないことを保証。

A9 – Using Components with Known Vulnerabilities

既知の脆弱性を持つコンポーネントの利用
ソースや、商用/オープンソースのコンポーネント、フレームワークを最新に保つことが重要
最も簡単な攻撃ベクトルで、その他の攻撃手法の突破口にもなる。
脆弱なサブコンポーネントに依存するコンポーネントの利用しているケース。
コンポーネントの脆弱性は、CVEにて管理/追跡されないので、緩和が難しい。

  • アプリケーション開発者は、それぞれのベンダー、作成者、またはプロバイダとのコンポーネントのステータスを個別に追跡する責任を負う。
  • 脆弱性は、既存のバージョンを修正するのではなく、新機能を含む新しいバージョンのコンポーネントで解決される。
  • そのため、アプリケーション開発者は、新バージョンの実装、テスト、デプロイの工数を負担する。

基本的な対処に

  • アプリケーションの依存関係と基礎となるコンポーネントの依存関係の識別と追跡
  • コンポーネントのセキュリティを追跡するための監視プロセス
  • コンポーネントのパッチやリリース頻度、許容されるライセンスモデルを考慮したソフトウェア開発プロセスとポリシーの確立
  • これらにより、コンポーネントプロバイダーがコード内の脆弱性に対処する際に、迅速に対応できる。

AWS WAFで何ができるか

  • アプリケーションで使用していないコンポーネントの機能に対するHTTPリクエストをフィルタリングおよびブロックによる、攻撃面の削減
  • 例)HTTPリクエストを直接/間接的にアセンブルするコードへのアクセスのブロックする
  • String-matchコンディションを用いたURI検査(例えば、”/includes/”の制限)
  • アプリケーションでサードパーティのコンポーネントを使用しているが機能のサブセットのみを使用する場合は、同様のAWS WAF条件を使って、使用しないコンポーネントの機能への公開URLパスをブロックする。

その他にできること

  • ペネトレーションテスト。マーケットプレイスで買える。申請が必要だが、一部事前承認を受けているサービスでは申請が不要なこともある。(マーケットプレイス上のマークで識別可能)
  • 展開とテストのプロセスに統合して、潜在的な脆弱性を検出するとともに、展開されたパッチが対象となるアプリケーションの問題を適切に緩和する。

A10 – Underprotected APIs

2017年版の新カテゴリ
特定アプリケーションの欠陥パターンではなく、潜在的な攻撃ターゲットを示す

  • UIを持たないアプリケーションは増えてきている。(UI/API両方使えるサービスも同様)
  • 攻撃ベクトルは、A1-A9までと変わらないことが多い。
  • APIはプログラムからのアクセスのために設計されているので、セキュリティテストでは、追加の考慮事項がある。
  • APIは、(Rest,SOAP問わず)複雑なデータ構造での動作とより広い範囲の要求頻度と入力値を使用するように設計される事が多い。

AWS WAFでできること

  • A1-A9での対応と変わらないが、対APIとして追加でできる事がある。
  • 強化が必要な主要コンポーネントは、プロトコルパーサ自体(XML,YAML,JSON 等)
  • パーサーの欠陥を悪用しようとする特定の入力パターンをString-match Condition、またはByte-Size Conditionを使用して、そのような要求パターンをブロックする。

旧OWASP Top10 A10 – Unvalidated Redirects and Forwards

リダイレクトや転送要求を検証しない場合、悪意のある当事者が正当なドメインを使用してユーザーを不要な宛先に誘導する可能性がある。

  • 例)短縮URLを生成する機能がある場合、URLジェネレータがターゲットドメインを検証しないことで、悪意を持ったユーザーが正式サービス上から悪意のあるサイトへの短縮URLを発行できる。

AWS WAFでできること

  • まず、アプリケーションでリダイレクトとフォワードがどこで発生するのかを理解する(リクエストパターン等)
  • アプリケーションが使用する、サードパーティコンポーネントも同様にチェックする。
  • エンドユーザからのHTTPリクエストに応じてリダイレクトと転送が生成された場合は、AWS WAFでリクエストをQUERY_STRINGやURIに対するString-Match Conditionでフィルタリングする(リダイレクト/転送の目的で信頼されているドメインのホワイトリストを維持する)

Cloud Formationテンプレート

Web ACLと、このドキュメントで推奨されている条件タイプとルールを含むAWS CloudFormationテンプレートが用意されています。
今回は、解説を割愛します。

所感

AWS WAFの設定は基本的にDIYですが、実装のポイントをOWASP Top10になぞらえて、その対応を解説する良いヒント集だと思いました。
AWS WAFを設定する人は、一読するのがいいですね。
外部にAWS WAFの設定を依頼する人は、アプリケーションの特性をよく理解している人を立てて、設定する人とよくコミュニケーションを取ることが重要だとわかります。
セキュリティ製品の真価を発揮するには、保護する対象のサービスへの理解とサービスへの適合化が必要なのは、どんなソリューションにも共通しますね。

今日はここまでです。
お疲れ様でした。

続きを読む