ブログをSSL化した

やらなきゃなーと思っていたこのブログのSSL化をようやくやった。 S3の静的ウェブサイトホスティングだったので、 ACMで証明書を発行してそれを使ってCloudFrontを通して配信している。

つまづきポイントは以下の3点。

証明書はus-east-1リージョンで作る必要がある

terraformのproviderにap-northeast-1を指定していたのでそこに作られてしまった。

terraform module内に専用のproviderを定義して、 aws_acm_certificateproviderを指定した。

Alternate Domain Namesの指定が必要

ChromeではERR_SSL_VERSION_OR_CIPHER_MISMATCHというエラーになる。 opensslコマンドで接続した場合は以下のような出力に。

$ openssl s_client -connect blog.handlena.me:443 -servername blog.handlena.me -debug
CONNECTED(00000006)
write to 0x7f9ad8e02260 [0x7f9ada00e003] (225 bytes => 225 (0xE1))
0000 - 16 03 01 00 dc 01 00 00-d8 03 03 14 fa 46 ce b9   .............F..
0010 - 38 2c 98 b7 c3 ea 20 a5-27 3d 2e c0 59 fa 55 20   8,.... .'=..Y.U
0020 - 7a 54 47 37 0d b2 18 89-41 97 0e 00 00 60 cc a9   zTG7....A....`..
0030 - cc a8 cc aa c0 30 c0 2c-c0 28 c0 24 c0 14 c0 0a   .....0.,.(.$....
0040 - 00 9f 00 6b 00 39 ff 85-00 c4 00 88 00 81 00 9d   ...k.9..........
0050 - 00 3d 00 35 00 c0 00 84-c0 2f c0 2b c0 27 c0 23   .=.5...../.+.'.#
0060 - c0 13 c0 09 00 9e 00 67-00 33 00 be 00 45 00 9c   .......g.3...E..
0070 - 00 3c 00 2f 00 ba 00 41-c0 11 c0 07 00 05 00 04   .<./...A........
0080 - c0 12 c0 08 00 16 00 0a-00 15 00 09 00 ff 01 00   ................
0090 - 00 4f 00 00 00 15 00 13-00 00 10 62 6c 6f 67 2e   .O.........blog.
00a0 - 68 61 6e 64 6c 65 6e 61-2e 6d 65 00 0b 00 02 01   handlena.me.....
00b0 - 00 00 0a 00 08 00 06 00-1d 00 17 00 18 00 23 00   ..............#.
00c0 - 00 00 0d 00 1c 00 1a 06-01 06 03 ef ef 05 01 05   ................
00d0 - 03 04 01 04 03 ee ee ed-ed 03 01 03 03 02 01 02   ................
00e0 - 03                                                .
read from 0x7f9ad8e02260 [0x7f9ada009e03] (5 bytes => 5 (0x5))
0000 - 15 03 03 00 02                                    .....
read from 0x7f9ad8e02260 [0x7f9ada009e08] (2 bytes => 2 (0x2))
0000 - 02 28                                             .(
4758201964:error:14004410:SSL routines:CONNECT_CR_SRVR_HELLO:sslv3 alert handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.200.4/libressl-2.6/ssl/ssl_pkt.c:1205:SSL alert number 40
4758201964:error:140040E5:SSL routines:CONNECT_CR_SRVR_HELLO:ssl handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.200.4/libressl-2.6/ssl/ssl_pkt.c:585:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 0 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Start Time: 1545553898
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

tfファイルではaws_cloudfront_distributionaliasesを指定すればいい。

/foo/bar//foo/bar/index.htmlを表示

S3から直接返していた場合はS3がindex.htmlを返してくれていた。 CloudFront経由かつoriginにS3バケット(BUCKET_NAME.s3.amazonaws.com)を指定した場合はこれが働かない。 S3の静的ウェブサイトホスティングが提供しているURL(BUCKET_NAME.s3-website-REGION.amazonaws.com)を指定すれば S3が返すindex.htmlをCloudFrontがそのまま返してくれる。

静的ウェブサイトホスティングをそのまま残すことになるが、 対象のバケットには公開するためのファイルしか置いていないので問題ないだろう。

参考: CloudFront に S3 bucket のサブディレクトリパスのコンテンツを参照させる - Qiita


terraform moduleは以下の通り。 variables.tfは省略。 外からはdomain_nameの指定ができるだけ。

locals {
  origin_id = "${lookup(var.website, "${terraform.workspace}.domain_name")}_origin"
}

provider "aws" {
  version = "~> 1.25.0"
  profile = "private"
  region  = "us-east-1"
  alias   = "us-east-1"
}

resource "aws_s3_bucket" "bucket" {
  bucket = "${lookup(var.website, "${terraform.workspace}.domain_name")}"
  acl    = "private"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::${lookup(var.website, "${terraform.workspace}.domain_name")}/*"
        }
    ]
}
EOF

  website {
    index_document = "index.html"
    error_document = "404.html"
  }
}

resource "aws_acm_certificate" "cert" {
  provider          = "aws.us-east-1"
  domain_name       = "${lookup(var.website, "${terraform.workspace}.domain_name")}"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_cloudfront_distribution" "distribution" {
  aliases = ["${lookup(var.website, "${terraform.workspace}.domain_name")}"]

  origin {
    domain_name = "${lookup(var.website, "${terraform.workspace}.domain_name")}.s3-website-ap-northeast-1.amazonaws.com"
    origin_id   = "${local.origin_id}"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.1", "TLSv1.2"]
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["HEAD", "GET"]
    cached_methods   = ["HEAD", "GET"]
    target_origin_id = "${local.origin_id}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    compress               = true
    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    max_ttl                = 0
    default_ttl            = 0
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = "${aws_acm_certificate.cert.arn}"
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.1_2016"
  }
}

前ふたつはAWSコンソールでポチポチしていれば気づいたはずだけど、 やる気を出して最初からtfファイルを書いてリソース作成したらデフォルト値になってしまっていた。

ほんとはDNS設定もここに書きたかったが、Route53管理ではないので手動設定。 .meも取れるようにならないかな・・・。

これで心おきなく年が越せる。