CubicLouve

Spring_MTの技術ブログ

Rails 5.2以降のRails 5系でDBに都度接続している場合においてはdatabase.ymlで `reaping_frequency: null` を設定する

Rails 5.2以降でDBに都度接続している場合においてはdatabase.ymlで reaping_frequency: null を設定しないと、スレッドが枯渇してサーバーにsshすらできなくなりサーバーの再起動が必要になります。

sshはこんなエラーになります。

shell request failed on channel 0

またサーバーに入っていると下記のようなエラーがでて何もできなくなります。

-bash: fork: retry: 子プロセスがありません
-bash: fork: リソースが一時的に利用できません

スレッドが枯渇しているサーバーは外部からみると、CPU、メモリなどの負荷がかかっているようには見えず、再起動して入ってみてもOOM-Killerなどが発動しておらず、何が起こっているかわかりにくいのが特徴です。

1プロセスあたりのファイルディスクリプタ数の上限も十分にあり、tcp周りの設定( net.ipv4.tcp_tw_reuse とか) もしてあり、file descriptorの枯渇もみられなかったので、原因がわからず焦りました。

ちなみに tcp_tw_recycle は廃止されたそうです。

qiita.com

git.kernel.org

原因

下記のPRにより、 reaping_frequency が設定されていないとデフォルトの60秒が設定されるようになりました。

github.com

これはReaperがDBへの接続がハングしているか死んでいるかをチェックする間隔になります。

api.rubyonrails.org

Reaperは別スレッドを立てて定期的に起き上がって確認する実装になっています。

Reaperは、establish_connectionが実行され、コネクションプールが生成されるたびにスレッドを作成します。

がこのスレッドは特にkillもされずに残る実装になっていそうです。

https://github.com/rails/rails/blob/1f9e308695556aae16872b54d9a0f0e1d7b3d055/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L359

2020/08/31時点のedgeを見ると、ちゃんと止める実装も入っている気がします。

https://github.com/rails/rails/blob/1f9e308695556aae16872b54d9a0f0e1d7b3d055/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L331-L356

で、activerecord-refresh_connectionなどを使って都度接続などを行っていると、コネクションを貼るたびにスレッドが作られ増え続けます。

github.com

スレッドが増える様子は pstree コマンドで確認できます。(下記の中で数字がスレッド数です)

# pstree
sh---bundle-+-bundle---73*[{bundle}]
            |-bundle---49*[{bundle}]
            |-bundle---178*[{bundle}]
            |-bundle---106*[{bundle}]
            |-bundle---122*[{bundle}]
            |-bundle---98*[{bundle}]
            |-bundle---130*[{bundle}]
            |-bundle---138*[{bundle}]
            `-74*[{bundle}]
// アクセスする
# pstree
sh---bundle-+-bundle---73*[{bundle}]
            |-bundle---49*[{bundle}]
            |-bundle---186*[{bundle}]
            |-bundle---106*[{bundle}]
            |-bundle---122*[{bundle}]
            |-bundle---98*[{bundle}]
            |-bundle---130*[{bundle}]
            |-bundle---138*[{bundle}]
            `-74*[{bundle}]

そうすると、スレッド数がもりもり増えていき、thread-maxに到達して、サーバーにアクセスできなくなります。

対応方法

reaping_frequency を明示的に null ないしは 0で指定すればそもそもReaperはスレッドを作らなくなります。

さらに都度接続しているのであれば、そのリクエスト内でコネクションがおかしくなったらエラーになりますが、次のリクエストを処理するときは再接続するのでエラーが出続けることはないので、上記の設定でも問題ないと考えています。

参照

thread-maxの設定

https://github.com/torvalds/linux/blob/7c2a69f610e64c8dec6a06a66e721f4ce1dd783a/Documentation/admin-guide/sysctl/kernel.rst#threads-max

bcryptの仕組み(とbcrypt-rubyの中身を見る)

Ruby(主にRails)でパスワードを扱う際のハッシュ化には bcrypt-ruby を使うことがほとんどだと思います。

github.com

bcryptについて勉強しつつ、さらに実装を確認してみようと思います。

bcrypt-rubyv3.1.15 を利用しています。

bcrypt

説明はwikipediaにあります。

ja.wikipedia.org

https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf

Blowfishの鍵セットアップ関数に Eksblowfish を使っていて、このセットアップにcostとソルトとパスワードを利用しています。

セットアップ後、 OrpheanBeholderScryDoubt という24 byteの文字列を64回繰り返し暗号化しバイト列を生成します。

bcrypt-ruby ではここで定義されています。

https://github.com/codahale/bcrypt-ruby/blob/4279fa01f083b48de9a18c54423453059074ba77/ext/mri/crypt_blowfish.c#L80-L87

そして、costとソルトと暗号化して生成されてバイト列を結合したものがbcryptがハッシュ値として生成する文字列になります。

bcrypt-ruby でハッシュ化された文字列

bcrypt-ruby でハッシュ化に使われるバージョンは 2a のみぽいです。

生成されるハッシュは接頭辞は $2a$ になっています。

irb(main):002:0> my_password = BCrypt::Password.create("my password")
=> "$2a$12$zafAQ3jKUpR7TXwYRm8nmeO.3p/1iZN9a09F7cFs7M6kNZxnbvf9C"
irb(main):003:0> my_password.version 
=> "2a"

ただし、他のバージョンでハッシュ化されたものも読み込めそうです。

irb(main):006:0> new_password = BCrypt::Password.new("$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e")
=> "$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"
irb(main):007:0> new_password.version
=> "2y"

https://github.com/codahale/bcrypt-ruby/blob/2121626ba841b4c1f0f3947d2f703cfe71fb977f/ext/mri/wrapper.c#L216-L234

ハッシュ化されている文字列はいくつかの区分に分けられます。

まず 最初の $ $ で 囲われている部分はバージョンです。

次に $ $で囲われているのは、コストの部分です。

これは、2を基数とした指数となり、この数の2の累乗がEksblowfishでのセットアップ中での繰り返し回数になります。(ストレッチング)

この回数が多ければ、ブルートフォース攻撃になどに対する対抗が強くなりますが、その分計算量が増えて計算に時間がかかるようになります。

bcrypt-ruby では BCrypt::Engine.calibrate() といいメソッドが用意されており、どれくらいのコストになるかをざっくりと計算できます。

irb(main):002:0> BCrypt::Engine.calibrate(5000) # この場合は5000ms以内で終わる最大のコストを算出してくる
=> 16

bcrypt-ruby ではハッシュ化のcostを指定できます。

irb(main):004:0> my_password = BCrypt::Password.create("my password", cost: 4)
=> "$2a$04$KV38A7SDgPKjmVi7ir59muRmquFxR/KmX/HtgWY97qC1tBryajHp."

ただし、4以下の場合は全て4に置き換えられます。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L67-L69

irb(main):005:0> my_password = BCrypt::Password.create("my password", cost: 1)
=> "$2a$04$vLCmqS2n9tnYpnKT.G9wJuhBPjtKakH2/9zOQuU1IIv.HIDj6q27e"  # コストが04になっている

また、costが31より大きいと ArgumentError の例外が発生します。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L45

コスト以降の部分はソルトとチェックサムになります。

128ビットのソルトになるのですが、Radix-64エンコードされて22バイトになっています。

チェックサムも184ビットなのですが、Radix-64エンコードされて31バイトになっています。

パスワードの確認する際は、 bcrypt-ruby ではハッシュ化された文字列のバージョンやコストも含む先頭から29バイトをソルトとして渡して処理をしていきます。

ハッシュ化

bcrypt-ruby ではソルトの生成におけるランダムな16バイトを生成するために OpenSSL::Random.random_bytes() を利用しています。

docs.ruby-lang.org

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L74

生成されたソルトを利用してハッシュ化をします。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L46

同じソルトを使えば、同じ文字列からは同じハッシュ値が生成されます。

irb(main):032:0> my_password = BCrypt::Password.create("my password")
=> "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm"
irb(main):033:0> BCrypt::Engine.hash_secret("my password", "$2a$12$VoGypMhauXeq4LZcLMbk6u")
=> "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm"

bcrypt-ruby の利用で気をつけること

Nullバイトの対応

bcrypt-ruby は Nullバイトの対応ができてないです。

irb(main):039:0> password = BCrypt::Password.create("foo\0bar")
=> "$2a$12$YPoekx0N24xH50Z0y9JfUeBpEBvYLNISVaSaH4oWJKAWnoTHa46RO"
irb(main):040:0> password == "foo"
=> true

github.com

PRはあるのですが、mergeされていませんね、、

なので、下記2つのブログで言及されているようなことで問題になります。

blog.tokumaru.org

qiita.com

パスワードの文字数は72文字まで

72文字以降は切り捨てられます。

irb(main):043:0> password = BCrypt::Password.create("a" * 73)
=> "$2a$12$CnOALTas/7Yj0JuzBW3GuOE1rKm3DZfN7N6OLwEGH8lnELV7H3iEC"
irb(main):044:0> password == "a" * 72
=> true

これは他の言語の実装でも同じような挙動になることが多いとのことです。

SecretSalt

bcryptでは OrpheanBeholderScryDoubt という文字列を暗号化することですが、この固定の文字列も変えてハッシュ値を取るのが SecretSalt になります。

qiita.com

Railsのforce_sslとssl_optionsについて

Railsforce_sslssl_options について知らないことが多かったのでまとめてみます。

railsguides.jp

force_ssl

force_ssl を有効にすると、ActionDispatch::SSLミドルウェアとして差し込まれます。

rails/default_middleware_stack.rb at 5394c94a2b8fa3b24893f00b2fc20d6554bf31c9 · rails/rails · GitHub

これにより、 全てのリクエストにHTTPSプロトコルが強制されます。

ちなみに、ActionDispatch::SSL はその他にも色々やってくれます。

  • HTTPからHTTPSへのリダイレクト
  • HSTSの設定
  • CookieにSecure属性をつける

rails/ssl.rb at 5394c94a2b8fa3b24893f00b2fc20d6554bf31c9 · rails/rails · GitHub

で、内部だけで使うヘルスチェックとかのエンドポイントに対してHTTPS強制されるとそれはそれで面倒です。

それを回避するために、ssl_options を使います。

ssl_options

ActionDispatch::SSL

ssl_options の中の redirect の設定の中で、HTTP => HTTPSのリダイレクトの対象から除外する exclude オプションがあります。

上記のページにもありますが、ヘルスチェックなどのパスを除外することができます。

config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }

他にもHSTSやCookieのSecure属性に対するオプションもあります。

参考

『プロフェッショナルSSL/TLS』www.lambdanote.com

ブラウザのXSSフィルタはどうなったのか(2020/08/14 時点)

ブラウザのXSSフィルタを有効にするために、 X-XSS-Protection レスポンスヘッダーでブラウザの設定を上書きして有効にするという内容が徳丸本のp 135にでてきます。

XSSフィルターってどういう機能なんだろうと調べてみたところ、下記記事にある通り、2020/08/14 現在だとほとんどのブラウザでなくなったりしていることがわかりました。

www.zdnet.com

Chrome

ChromeではXSS Auditorという名前でXSSフィルターを実装していましたが、Chrome 78でこの機能が削除されました。

www.chromium.org

で、この機能の代わりにChrome 83で Trusted Types という新しい機能をリリースしています。

developers-jp.googleblog.com

web.dev

使い方は下記を参考にするとよさそうです。

sbfl.net

Firefox

FirefoxX-XSS-Protection レスポンスヘッダーを使ったXSSフィルターは実装していないですね。

bugzilla.mozilla.org

Safari

Safariは2020/08/14でも使えてそうですね。

IE Edge

EdgeだとXSSフィルターは削除されています。 IEはまだ使えてそうですね。

blogs.windows.com

VS Codeでフォルダの階層をまとめない

Compact Folders を off

or

settings.jsonで下記を設定

"explorer.compactFolders": false,

https://code.visualstudio.com/updates/v1_41#_compact-folders-in-explorer

いつも忘れるのでいい加減メモっておく

React Navigationを4系から5系にバージョンアップする

ReactNative は 0.63.2 まで更新してある状態で、React Navigationのバージョンを4系から5系にバージョンアップしました。

その時の対応をまとめます。

React Navigation 5のアップデート内容

下記にまとまっています。

reactnavigation.org

ここで取り上げられているテーマは下記の通り

  • React コンポーネントを使った設定
  • 新しいフック
  • コンポーネントからのオプションの更新
  • 新しいテーマAPI
  • TypeScript化(型を第一級に扱う)
  • Redux DevTools との統合
  • ネイティブなStack Navigator
  • ネイティブなMaterial top tab navigator

npm packageの更新

https://reactnavigation.org/docs/upgrading-from-4.x#package-names

使っていたライブラリについて、下記のように変更

react-navigation -> @react-navigation/native
react-navigation-stack -> @react-navigation/stack
react-navigation-tabs -> @react-navigation/bottom-tabs

@react-navigation/material-top-tabs@react-navigation/material-bottom-tabs は今のプロジェクトでは不要だったのでインストールしませんでした。

また、v4との互換性を保つための仕組みを提供してくれる @react-navigation/compat を使いました。

=> 最終的に使うのをやめました。

バージョンアップの方針

下記に4系からのアップデートについての方法が公式ページにあります。

Upgrading from 4.x | React Navigation

ここでもある通り、React Navigation 4 のメンテナンスは当面は継続して行い、React Native の最新バージョンにも追従すると書かれています。

いつまでメンテしてくれるかはなんか不安ですが、今すぐに開発が止まるということがなさそうなので、すぐに無理にバージョンアップしなくてもよさそうです。

今のプロジェクトではアップデートする時間を作れたのでアップデートを実施しました。

@react-navigation/compat を使う @react-navigation/compatは使わずにアップデートしました

React Navigation 5では4とはほぼ互換性がない状態です。

なので、いきなり全部対応しようとすると量が膨大になるので、まずはReact Navigationが用意した @react-navigation/compat を利用し、変更を最小限にしました。

今後の運用も考えて @react-navigation/compat は使わずにアップデートを行いました。

変更箇所

createStackNavigator createBottomTabNavigator を使ったNagvigator

v5での createXNavigator は 大幅に変更されており、React コンポーネントとしてnavigatorを設定するようになっています。

https://reactnavigation.org/docs/upgrading-from-4.x/#configuring-the-navigator

createStackNavigator | React Navigation

なので、ここは一旦 @react-navigation/compatcreateCompatNavigatorFactory を使って既存の呼び出しをラップしました。

Navigatorの数が少なかったのと今後の運用を加味して、v5の方式に移行しました。

v5ではScreenで柔軟にオプションを指定できるようになったため、同時にNavigatorの階層を見直し階層を減らしています。

下記に書いてあるとおり、今回のアップデートではかなり大きな方針変更がありました。

https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator

v4以前では、遷移は静的に定義されていたのに対し、v5では動的に設定されるようになりました。

v4以前では、再帰的にScreenを探しにいきますが、v5ではそれがなくなりました。

v5では、現在のNavigator内で遷移できなければ、一階層上の親のNavigatorから探しに行きますが、それより上には行けないようです。

そのため、Navigatorの階層は2つくらいに抑えることが推奨されています。

When nesting multiple stack navigators, we recommend nesting at most 2 stack navigators, unless absolutely necessary.

Nesting navigators | React Navigation

createSwitchNavigator@react-navigation/compat のものを使う createSwitchNavigatorは利用をやめました

React Navigation 5 では、React コンポーネントを使うようになったため、条件分岐なども書けるようになりました。

そのため、NavigatorのScreen定義を、動的に定義して変更することができるので、Switch Navigatorが不要になります。

なので、createSwitchNavigator は廃止されているのですが、 今回修正範囲が大きかったので、一旦 @react-navigation/compat にある、createSwitchNavigator を使いました。

それに従い、 createStackNavigator に置き換えて対応しました。

NavigationStackScreenPropsStackNavigationProp に置き換える

v5では NavigationStackScreenPropsStackNavigationProp となりました。

v4

https://reactnavigation.org/docs/4.x/typescript/#type-checking-all-props-for-a-screen

v5

https://reactnavigation.org/docs/typescript/#type-checking-screens

これにより、Screenに対するparameterを型チェックできるようになります。

修正はこんな感じでした。

navigation.navigate を使った遷移にも型チェックが効くようになっています。

下記のような感じで全般的に書き直しました。

Screen名 + ParamList というtypeを用意しつつ、exportしています。

import { BarParamList } from 'src/screens/Bar'

export type FooParamList = {
  Foo: {}
}

interface Props {
  navigation: StackNavigationProp<FooParamList & BarParamList>
}

export function Foo({ navigation }: Props) {
  useEffect(() => {
    if (!isLoading && isLoggedIn) {
      navigation.navigate('Bar', {})
    }
  }, [isLoading, isLoggedIn, navigation])
  return (
    <Wrapper>
      <FooContent />
    </Wrapper>
  )
}

useNavigation@react-navigation/native のものを使う

これはimportの変更のみです。

-import { useNavigation } from 'react-navigation-hooks'
+import { useNavigation } from '@react-navigation/native'

Sceenには前のScreenから navigationとrouteを受け取るようになりました。

https://reactnavigation.org/docs/params#passing-params-to-a-previous-screen

そのため、Screenでは useNavigation を使わず、渡ってくるnavigationを使うようにしました。

パラメーターの受け渡しの変更、 getParamの廃止

https://reactnavigation.org/docs/upgrading-from-4.x/#the-navigation-prop

React Navigation 5 では、navigation propは2つのpropに分割されています。

  • navigation prop : navigategoBack などのヘルパーメソッドを含む
  • route prop : 現在の画面のデータを含みます。 これは 4とかだと navigation.state を使っていたものです。

また、getParam は廃止されました。

getParam は主に2つの役割がありました。

  • paramsundefined になることを防ぐ
  • params.someParam が未定義または null であった場合においてデフォルト値を設定する

これらは、TypeScriptの optional chainingnullish coalescing operators で同等のことができます。

Passing parameters to routes | React Navigation

v5ではこのようになりました。

export function Foo({ route, navigation }: Props) {
  const uniqueKey = route.params.uniqueKey
  return (
    <ScrollView>
      <Block>
        <FooContetn uniqueKey={uniqueKey} />
      </Block>
    </ScrollView>
  )
}

createAppContainer から NavigationContainer を使うように変更

React Navigation 5 では createAppContainer は廃止されました。

下記のドキュメントのように、NavigationContainer を使うように修正しただけでした。

https://reactnavigation.org/docs/upgrading-from-4.x#navigation-container

参考資料

Upgrading from 4.x | React Navigation

Compatibility layer | React Navigation

Passing parameters to routes | React Navigation

Type checking with TypeScript | React Navigation

React Navigation v5 傾向と対策 - Qiita

GKE上で動くPodでeBPFを使ってみる

GKEのバージョン

1.16.13-gke.1

Node

Container-Optimized OS from Google

コンテナのOS

OSは下記を使うのでちょっと特殊

https://console.cloud.google.com/gcr/images/gcp-runtimes/GLOBAL/ubuntu_18_0_4?gcrImageListsize=30

# cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.4 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
# uname -r
4.19.112+

コンテナのbpfの状態

zgrep -i bpf /proc/config.gz
CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_NETFILTER_XT_MATCH_BPF=m
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=m
# CONFIG_NET_ACT_BPF is not set
CONFIG_BPF_JIT=y
# CONFIG_BPF_STREAM_PARSER is not set
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
# CONFIG_BPF_KPROBE_OVERRIDE is not set
CONFIG_TEST_BPF=m

準備

Podの設定

privileged modeを有効にしておく

https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged

securityContext:
  privileged: true

Podの中での作業

bcc/INSTALL.md at master · iovisor/bcc · GitHub を参考にしながらやってみる

wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19.112/linux-headers-4.19.112-0419112_4.19.112-0419112.202003200739_all.deb
wget https://kernel.ubuntu.com/~kernel-ppa/mainline/v4.19.112/linux-headers-4.19.112-0419112-generic_4.19.112-0419112.202003200739_amd64.deb
dpkg -i *.deb
mkdir -p /lib/modules/4.19.112+/
# 確認コマンド
ls /lib/modules/4.19.112-0419112-generic/build
ln -s /lib/modules/4.19.112-0419112-generic/build /lib/modules/4.19.112+/build

#  https://github.com/iovisor/bcc/blob/master/INSTALL.md#ubuntu---binary 参照してiovisor packages (Upstream Stable and Signed Packages) をいれる
apt-get install gnupg
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/iovisor.list
apt-get update
apt-get install bcc-tools

結果その1 (privileged modeが無効の場合)

# /usr/share/bcc/tools/execsnoop
could not open bpf map: events, error: Operation not permitted
Traceback (most recent call last):
  File "/usr/share/bcc/tools/execsnoop", line 165, in <module>
    b = BPF(text=bpf_text)
  File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 325, in __init__
    raise Exception("Failed to compile BPF text")
Exception: Failed to compile BPF text

権限不足になる

結果その2 (privileged modeが有効の場合)

# /usr/share/bcc/tools/execsnoop
PCOMM            PID    PPID   RET ARGS
gke              1684128 ?        0 /home/kubernetes/bin/gke
portmap          1684133 ?        0 /home/kubernetes/bin/portmap
bandwidth        1684137 ?        0 /home/kubernetes/bin/bandwidth
timeout          1684141 ?        0 /usr/bin/timeout 60 docker ps

おおおおお

うまくいった

参照

http://ubuntuhandbook.org/index.php/2018/10/linux-kernel-4-19-released-install-ubuntu/

Index of /~kernel-ppa/mainline/v4.19.112

github.com

github.com

go-vargo.hatenablog.com