AWSのコンソルにパスワード無しログイン

## はじめに
社内で社員や色んなサービスがTLSクライアント証明書を使います。社内のアイデンティティ管理システムとAWSに関係になりたいから、このGatewayのシステムを開発しました。

TLSクライアント証明書は不要。 SAML、OAuth2などがこの認証パタンを使う可能性です。

### 信用モデル
![trust.png](https://qiita-image-store.s3.amazonaws.com/0/155859/24286444-673f-aa95-b6a4-dbd2d8f005ea.png)
<details><summary><sub>Graphvizのソース</sub></summary><p>

```
digraph trust {
  graph[style="filled" color="#ffffff" fontcolor="#333333" fontname="sans" fontsize="16" rankdir="LR"];
  node[shape="rect" color="#333333" fontcolor="#333333" fillcolor="#f8f8f8" style="filled" fontname="sans"];
  edge[fontname="sans" color="#79b74a" fontcolor="#333333" fontsize="12"];

  engineer[label="エンジニア"]
  gateway[label="Gateway"];
  ca[label="社内のCA"];
  iam[label="AWS IAM"]

  engineer -> gateway[label="TLS 相互認証" dir="both"];
  gateway -> ca[label="CRL/OCSP"];
  gateway -> iam[color="#eea236" label="sts:assumeRole"];
  iam -> gateway[color="#eea236" label="認証情報セット"];
}
```

 </p></details>
このGatewayの責任は認証、そしてIAMは認証と認可です。二つのIAMロールがある：Gatewayのインスタンスプロファイルとエンジャニヤの個人的なロール。GatewayがTLSクライアント証明書のSubjectを読むからデータベースが不要です。絶対にGatewayがエンジニアの認証情報セットを保存してません。

## アーキテクチャ
![logical.png](https://qiita-image-store.s3.amazonaws.com/0/155859/03cd2a0e-1b8d-1472-b1bd-0679fcb2617f.png)
<details><summary><sub>Graphvizのソース</sub></summary><p>

```
digraph architecture {
  graph[style="filled" color="#333333" fillcolor="#ffffff" style="filled" fontcolor="#333333" fontname="sans" fontsize="16" rankdir="LR"];
  node[shape="rect" color="#333333" fontcolor="#333333" fillcolor="#f8f8f8" style="filled" fontname="sans"];
  edge[fontname="sans" color="#79b74a" fontcolor="#333333" fontsize="12"];

  engineer[label="エンジニア" color="#ffffff" fillcolor="#ffffff"];
  iam[label="IAM" color="#ffffff" fillcolor="#ffffff"];

  subgraph cluster0{
    graph[label="VPC" labelloc="b" style="dashed"];
    elb[label="ELB" fixedsize="true" height="2" width="0.5"];
    subgraph cluster1 {
      graph[label="EC2" labelloc="t" labeljust="r" style="filled"];
      apache[label="Apache"];
      gateway[label="Gateway"];
    }
    nat[label="NAT"];
  }
  engineer -> elb[label="https://gateway/"];
  elb -> apache[];
  apache -> gateway[color="#eea236"];
  gateway -> nat[dir="none"];
  nat -> iam[];
}
```

 </p></details>

API GatewayがTLSクライアント証明書が使わないからELBとEC2は必要です。そしてTLSのセッションがエンジニアからApacheまで、ELBのリスナーはTCP:TCPです。[Proxy Protocol のサポート](http://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/enable-proxy-protocol.html)とApacheの[mod_proxy_protocol](https://github.com/roadrunner2/mod-proxy-protocol)を設定しない限り、クライアントのIPアドレスは受け取りません。

### ログインのシーケンス
![diagram-4989406697349261473.png](https://qiita-image-store.s3.amazonaws.com/0/155859/694941d2-d933-5575-a54a-9c35e54959d7.png)
<details><summary><sub>PlantUMLのソース</sub></summary><p>

```
@startuml

skinparam monochrome true
skinparam shadowing false

エンジニア -> Gateway: /login?account=1234
Gateway -> Gateway: Validate Certificate \n (CRL/OCSPなど)
Gateway -> STS: assumeRole
STS -> Gateway: 認証情報セット
Gateway -> エンジニア: 302 https://signin.aws.amazon.com/federation?
エンジニア -> "フェデレーションエンドポイント": GET /federation
"フェデレーションエンドポイント" -> エンジニア: 302 https://console.aws.amazon.com/
エンジニア -> コンソール: GET /
コンソール -> エンジニア: 200

@enduml
```

 </p></details>
上の図は、最後のリクエストから追加のリクエストがありますから完成しません。

## 開発のメモ

#### リバースプロキシサーバーの設定

```apache
Listen 443
<VirtualHost *:443>

SSLEngine On
SSLVerifyClient require
SSLVerifyDepth 2

# 他の設定はこちら

RequestHeader unset client-ssl-certificate 
RequestHeader set client-ssl-certificate "/%{SSL_CLIENT_S_DN_O}s/%{SSL_CLIENT_S_DN_OU}s/%{SSL_CLIENT_S_DN_Email}s/"

ProxyPass / https://localhost:8080
ProxyPassReverse / https://localhost:8080
</VirtualHost>
```
そのRequestHeaderのunsetの前にsetはヘッダインジェクションの対策です。


#### Gatewayの例
```js
'use strict';

var express = require('express'),
    AWS     = require('aws-sdk');

var app = express();

app.get('/login', function(req, res){

  var params = {
    'RoleArn': 'arn:aws:iam::' + req.query.account + ':role/' + req.header('x-ssl-certificate'),
    'RoleSessionName': 'Gateway'
  };

  sts.assumeRole(params, function(err, data) {
    if(err){
      res.status(400).send('Access Denied.').end();
    } else {
        var signInToken = encodeURIComponent(JSON.stringify({
            'sessionId':    data.credentials.accessKeyId,
            'sessionKey':   data.credentials.secretAccessKey,
            'sessionToken': data.credentials.sessionToken
        }));
        var redirectUrl = [
          'https://signin.aws.amazon.com/federation',
          '?Action=login',
          '&Issuer='     , encodeURIComponent('https://gateway/'),
          '&Destination=', encodeURIComponent('https://console.aws.amazon.com/'),
          '&SigninToken=', signInToken].join('');
        res.redirect(302, redirectUrl).end();
    }
  });
});

app.listen(8080);
```

#### エンジニアのロール
AWSのアカウントで、全てのエンジニアのために、新しいのロールを設定するべきです。

##### CloudFormation例

```json

{
  "Type": "AWS::IAM::Role",
  "Properties": {
    "RoleName": "pnktsrmn",
    "Path": "/Company_CA/Department_OU/pnktsrmn@himitsukaisha.com/",
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "",
          "Effect": "Allow",
          "Principal": {
            "AWS": "arn:aws:iam::1234567890:role/gateway-role"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }
  }
}
```

PrincipalはGatewayのインスタンスプロファイルのARN。パスはTLSクライアント証明書のSubjectです。

##参考
* [フェデレーションユーザーに対して AWS マネジメントコンソール へのアクセスを許可する URL の作成](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html)
* [CloudFormation AWS::IAM::Role](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html)
* [STS AssumeRole (英語)](http://docs.aws.amazon.com/ja_jp/STS/latest/APIReference/API_AssumeRole.html)