IAMユーザにMFAを強制しつつterraformする

AWSのIAMユーザで,コンソールログインする際には必ずMFAを強制したい,と思うことがある.

というわけでそういうIAM Policyを作って当ててみた.

IAM Policyでの強制

だいたいこういうのを参考にすると,そのまま使えるのがある.

blog.vtryo.me

ただし,注意すべきことがある. 最後の,

{
  "Sid": "BlockMostAccessUnlessSignedInWithMFA",
  "Effect": "Deny",
  "NotAction": [
      "iam:CreateVirtualMFADevice",
      "iam:DeleteVirtualMFADevice",
      "iam:ListVirtualMFADevices",
      "iam:EnableMFADevice",
      "iam:ResyncMFADevice",
      "iam:ListAccountAliases",
      "iam:ListUsers",
      "iam:ListSSHPublicKeys",
      "iam:ListAccessKeys",
      "iam:ListServiceSpecificCredentials",
      "iam:ListMFADevices",
      "iam:GetAccountSummary",
      "sts:GetSessionToken",
      "iam:CreateLoginProfile",
      "iam:ChangePassword"
  ],
  "Resource": "*",
  "Condition": {
      "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
      }
  }
}

ここで,ConditionBoolIfExists を使っている.

この条件式の詳細は以下の通り.

docs.aws.amazon.com

aws:MultiFactorAuthPresent に関して,

Deny、BoolIfExists、false のこの組み合わせは、MFA を使用して認証されないリクエストを拒否します。具体的には、MFA を使用しないで一時的認証情報を使用して行われたリクエストを拒否します。また、IAM ユーザーアクセスキーなどの長期的認証情報を使用して行われたリクエストも拒否します。*IfExists 演算子は、aws:MultiFactorAuthPresent キーが存在するかどうか、MFA が使用されているかどうかを確認します。これは、MFA を使用して認証されないリクエストを拒否する場合に使用します。

との記述がある.

これは即ちアクセスキーを用いたアクセスも拒絶されることになる.大抵の場合,ローカルからterraform applyする際には,自分のIAMユーザのアクセスキーを使っていることが多い.

Deny,BoolIfExists,falseの組み合わせを使うと,そういったアクセスまですべて拒否することになる. また,あまりよい構成ではないが,たとえばアプリケーションからAWSのサービスを呼び出す際に,アクセスキーを使っているようなパターンがある場合,この条件に引っかかってしまう.

そもそもMFA強制のポリシーをシステムユーザやIAM Roleに充てることは,意図してやりたいことではないと思うが.

terraformが使える状態のポリシー

アクセスキーからのアクセスを許可するためには,

{
  "Sid": "BlockMostAccessUnlessSignedInWithMFA",
  "Effect": "Deny",
  "NotAction": [
      "iam:CreateVirtualMFADevice",
      "iam:DeleteVirtualMFADevice",
      "iam:ListVirtualMFADevices",
      "iam:EnableMFADevice",
      "iam:ResyncMFADevice",
      "iam:ListAccountAliases",
      "iam:ListUsers",
      "iam:ListSSHPublicKeys",
      "iam:ListAccessKeys",
      "iam:ListServiceSpecificCredentials",
      "iam:ListMFADevices",
      "iam:GetAccountSummary",
      "sts:GetSessionToken",
      "iam:CreateLoginProfile",
      "iam:ChangePassword"
  ],
  "Resource": "*",
  "Condition": {
      "Bool": {
          "aws:MultiFactorAuthPresent": "false"
      }
  }
}

こうする.

Deny 効果、Bool 要素、false 値のこの組み合わせは、MFA を使用して認証できるが認証されなかったリクエストを拒否します。このステートメントは、MFA の使用をサポートする一時的認証情報にのみ適用されます。このステートメントは、長期的認証情報を使用して行われたリクエスト、または MFA を使用して認証されたリクエストへのアクセスを拒否しません。ロジックが複雑で MFA 認証が実際に使用されたかどうかをテストしないため、この例は慎重に使用してください。

とあるので,アクセスキーによる認証は許可されている.

ただし,これでも,一部terraform planが失敗した.

原因は,EC2 Spot Fleetに関するterraform定義が,Access Deniedになるためであった.

上記のポリシーでは,アクセスキーによるアクセスが拒否されないのになぜ?

実は,Spot Fleetはユーザが直接操作を行うものだけではなく,

ユーザーの代わりにインスタンスの入札、起動、タグ付け、削除を行う権限をスポットフリートに付与するためのロールを作成し、それをスポットフリートのリクエストで指定する必要があります。

という記述がある.

そのため,上記のようなポリシーが,SpotFleetの操作を行うRoleの認証を拒絶している可能性が高い.

これに対しては,PrincipalTypeを検証することで,Denyを避けることができる.

PrincipalTypeに関する条件はここに,

docs.aws.amazon.com

PrincipalTypeの種別はここに書いてある. docs.aws.amazon.com

で,以下のようなPolicyを作る.

{
  "Sid": "BlockMostAccessUnlessSignedInWithMFA",
  "Effect": "Deny",
  "NotAction": [
      "iam:CreateVirtualMFADevice",
      "iam:DeleteVirtualMFADevice",
      "iam:ListVirtualMFADevices",
      "iam:EnableMFADevice",
      "iam:ResyncMFADevice",
      "iam:ListAccountAliases",
      "iam:ListUsers",
      "iam:ListSSHPublicKeys",
      "iam:ListAccessKeys",
      "iam:ListServiceSpecificCredentials",
      "iam:ListMFADevices",
      "iam:GetAccountSummary",
      "sts:GetSessionToken",
      "iam:CreateLoginProfile",
      "iam:ChangePassword"
  ],
  "Resource": "*",
  "Condition": {
      "Bool": {
          "aws:MultiFactorAuthPresent": "false"
      },
     "StringEquals": {
          "aws:PrincipalType": ["account", "user"]
      }
  }
}

Conditionブロック内の複数の条件式はANDで効く.そのため,PrincipalTypeがaccount, userの場合 && MFAでない場合にのみ,Denyが発動するようになっている. 幸いにもSpotFleetのリクエストを行うRoleはaccountでもuserでもない.そしてコンソールからのログインはuserに該当する.

ただ確かにAWSのドキュメントに書いてあるとおり,複雑な認証フローになることは否めない.

注意点

最後に注意点を書いておくと,以上のようなPolicyをいじる際,普段terraform applyを行っているアカウント(例えば自分のアカウント)に対して,上記のようなPolicyをいきなり当ててしまうのは大変リスキーである. 例えば,最初に提示したような,アクセスキーからのアクセスを禁止するようなPolicyを当ててしまった場合,その後terraform applyは不可能になってしまうので,一度コンソールからログインして,自分のアカウントに付与されているPolicyを変更する必要がある.

また,うっかりするとログインすら拒絶するPolicyを当てることにも可能である.

そのため,こういうことをする場合は実験用アカウントを作成した上で,そのアカウントにのみPolicyを適用しながら,十分に検証した上で自分のアカウントにも付与しよう.

テレビを耐え抜く小手先技術

この記事は ex-crowdworks Advent Calendar 2018 の21日目です.

CrowdWorksを退職して8ヶ月くらいが経った.

CrowdWorksにいたときはSREをしていたので,テレビ放映があるときには少し覚悟が必要だったんだけど,その話をしようと思う.

あと,ここで書く話は,ただの昔話 であり,現在ではテレビが突然来ても割と耐えられるくらいの状態にはなっていると思う.ので,こんなことをしなくても凌げると期待している.

基本的にCrowdWorksは,クラウドソーシングのサービスなので,ニュースサイトやゲームのサービスほど,普段から大量の接続をさばく必要はない. そのため,そういう人たちにとっては物足りないくらいの対策でしかないと思うことを断っておく.

続きを読む

AWS ECS上にfluentdクラスタを構築し別のVPCからログを送りつける

この記事は scouty Advent Calendar 2018 の17日目です.

fluentdに関して,勢い余って今年こういう発表をした.

speakerdeck.com

完全に酔っていたので勢いだけなのだが,その後マトモなfluentdクラスタができたので書こうと思う.

もう二度とchefで作ろうとは思うまい.

続きを読む

Whalebirdの重かった部分の話

この記事は Mastodon Advent Calendar 2018 の10日目です.

普段はMastodonのデスクトップ向けクライアントWhalebird を開発しています.

https://whalebird.org

あと,Pleromaのインスタンスを運用してます.全然人が少ないし特に何も運営らしきことはしていないんだけど.

https://pleroma.io

というわけで今日はマストドンクライアントの話です. Whalebirdの実装の中でも,結構重かった(いろんな意味で)部分の話をします.

フロントは全部自分で書く必要がある

WhalebirdはElectronベースのMastodonクライアントですが,ただWebページを表示するだけのガワアプリではなく,フロントを全部自前実装してAPIを叩いているアプリです. そのため,前提として,Mastodon本体のフロントエンドの資産はほぼ使えません.

また基本的にAPIが用意されていない機能も使うことができません. そのため,これから書くような「Webの再実装に近いんだけど,全部自分で作らないといけない」状態になってます.

サジェスト

Whalebirdには

のサジェスト機能があります.

これらのサジェストを実装していく上で,考慮点がいくつかありました.

  • 検索は都度APIを叩くのか,起動時にキャッシュするのか
  • 都度APIを叩くとして,キー入力イベント全てに対してAPIコールしていいのか

こういうのに迷ったときにはMastodonのWebを参考にするんですが,Webでは

  • 都度APIを叩いている
  • キー入力イベント全てに対してAPIコールしている

という実装でした. というわけで,Whalebirdでも検索は都度APIを叩くことにしました. ただ,流石にサードパーティー製のアプリケーションで,APIコールしまくるのはどうかと思ったので.キー入力イベントは間引きならがAPIを叩いています.

そのため,基本的にここの部分の速さはインターネットの速度によって制限されます.まぁPC向けなのでそこまで遅い環境で使われることは想定しなくていいかなと思ってます.

キー入力イベントを間引いたとしても,キー入力のたびに

  • 入力文字列のパーサ
  • パースされた結果に応じて検索APIの呼び出し

という処理が裏で走るので,ここはかなりCPUを食う場所です.

2.5.0あたりでやたら入力がもっさりしたバージョンをリリースしましたが,あれの原因はほぼこいつらでした

あそこで,汚かった実装をいろいろとリファクタリングして,現状はこんな感じ.

const textAtCursorMatch = (str, cursorPosition, separators = ['@', '#', ':']) => {
  let word

  let left = str.slice(0, cursorPosition).search(/\S+$/)
  let right = str.slice(cursorPosition).search(/\s/)

  if (right < 0) {
    word = str.slice(left)
  } else {
    word = str.slice(left, right + cursorPosition)
  }

  if (!word || word.trim().length < 3 || separators.indexOf(word[0]) === -1) {
    return [null, null]
  }

  word = word.trim().toLowerCase()

  if (word.length > 0) {
    return [left + 1, word]
  } else {
    return [null, null]
  }
}

export default textAtCursorMatch

ここは本家のソースをだいぶ参考にしました.

github.com

で,現状は普通に使っててストレスないくらいにはなっていますが,やはり多少CPUは使いますね…….

絵文字

もう一つ2.5.0で重くなった要因があります. それが,EmojiMartの絵文字パレットの追加です.

これはMastodon本家に合わせて,EmojiMartのvue版を使っていました.

github.com

ただ,こいつのロードになかなか時間がかかるため, v-show で制御していました.これが重くなる原因で,この重さのコンポーネントを常に裏側に用意しておくと,またメモリを500MB以上食うようになりました.

というわけで最近こいつは v-if で制御するようにしています. そうすると絵文字パレットの表示には時間がかかるんですけどね…….

パーサ

Whalebirdのトゥートにはパーサが仕込まれています. これは,クリック時に以下の判定を行い,それぞれの動作に遷移させるために必要な処理です.

  • クリックされた文字列がアカウント名であれば,アカウントの詳細ページを開く
  • クリックされた文字列がハッシュタグなら,ハッシュタグタイムラインを開く
  • クリックされた文字列が上記に該当しないリンクであれば,OSのデフォルトブラウザで開く

また,これに関してはweb側の実装が一切参考にできません(Reactなので,おそらくパースなんかしなくてもRouterさえちゃんと設定してあれば画面遷移できるので).

というわけで,

export function findAccount (target, parentClass = 'toot') {
  if (target.getAttribute('class') && target.getAttribute('class').includes('u-url')) {
    return parseMastodonAccount(target.href)
  }
  // In Pleroma, link does not have class.
  // So we have to check URL.
  if (target.href && target.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/@[a-zA-Z0-9-_.]+/)) {
    return parseMastodonAccount(target.href)
  }
  // Toot URL of Pleroma does not contain @.
  if (target.href && target.href.match(/^https:\/\/[a-zA-Z0-9-.]+\/users\/[a-zA-Z0-9-_.]+/)) {
    return parsePleromaAccount(target.href)
  }
  if (target.parentNode === undefined || target.parentNode === null) {
    return null
  }
  if (target.parentNode.getAttribute('class') === parentClass) {
    return null
  }
  return findAccount(target.parentNode, parentClass)
}

export function parseMastodonAccount (accountURL) {
  const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/(@[a-zA-Z0-9-_.]+)/)
  const domainName = res[1]
  const accountName = res[2]
  return {
    username: accountName,
    acct: `${accountName}@${domainName}`
  }
}

export function parsePleromaAccount (accountURL) {
  const res = accountURL.match(/^https:\/\/([a-zA-Z0-9-.]+)\/users\/([a-zA-Z0-9-_.]+)/)
  const domainName = res[1]
  const accountName = res[2]
  return {
    username: `@${accountName}`,
    acct: `@${accountName}@${domainName}`
  }
}

こういうややこしいパーサをいくつか書いています.

リンクを外部ブラウザで開くだけなら,そこまで苦労するものでもないんですが,その手前にハッシュタグとアカウントのパーサを挟もうと思うと,これしかありませんでした.

こいつが重い要因は,要素の親を再帰的に辿るからです. これには理由があって,MastodonAPIが返してくるstatusはhtml構造となっており,

<p>
  <span>
    <a href="https://social.mikutter.hachune.net/@h3_poteto">
      @<span>h3_poteto</span>
    </a>
  </span>
  hogehoge
</p>

こんな感じで,必ずしもクリックした要素が<a> タグになっているわけではないからです.

また,MastodonとPleromaでこのhtml構造が微妙に違う場合があって,それらの差異もこのパーサですべて吸収しています.

まとめ

めちゃくちゃフロントの話になってしまいましたが,そのくらいバックエンドでボトルネックになるところは少ないです. 今のところMastodonAPIはよくできているので,裏側でストリーミング更新したりする部分は,ほとんど重くならずに実装できています.

実は最近の更新で,裏でストリーミングするタイムラインを選べるようになっているんだけど,そこでストリーミング数を増やしても,メモリ消費的にはそこまで上がりません. CPUは少し食うかもしれない,それはWebSocketなので仕方がないのだけれど…….

f:id:h3poteto:20181210231202p:plain

f:id:h3poteto:20181210231207p:plain

というわけで,実装的にも動作的にも重い部分を紹介しました. この辺はできればもっとスマートに高速化していきたいとは思っているので,今後にご期待ください.