koukiblog

たぶんweb系の話題

k8sのreadOnlyRootFilesystemを有効にして、Rackアプリケーションでファイルアップロードをするための設定

kubernetesにはreadOnlyRootFilesystemというオプションがあり、このオプションを有効にするとコンテナ内がreadonlyになるためセキュリティの施策の1つとして有効にされることがあります。その状態でRack(Rails,Sinatra,etc)アプリケーションからファイルアップロードを行うときに思ったより手間取ったのでそのときの対応メモ。

Rubyのバージョンは2.6。kuberntesはGKEを利用していて、1.11.7-gke.6でした。

前提とエラーの内容

WebアプリケーションでPOSTのファイルアップロードを受け付けるにはディスクに一時ファイルを書き込みできる必要があります。readOnlyRootFilesystemを有効にした状態でファイル書き込みを許可する場合には、k8sのemptyDirを作成し、それをマウントするのが一般的に取られる設定です。(おそらくですが)

以下のようなyamlになります。

   containers:
        image: rubyapp
        name: app
        securityContext:
          readOnlyRootFilesystem: true
        volumeMounts:
          - name: tmp
            mountPath: /tmp
     volumes:
        - name: tmp
          emptyDir: {}  

nginxのpid書き込みなど、この設定で問題ないのですが、Rackの場合はパーミッションエラーがありうまくいきませんでした。

エラーメッセージをよく見てみると一時ファイルを書き込みしようとしてるディレクトリがなぜかRubyを実行しているカレントディクトリになっています。Rubyの問題のようなので調べて見ると、Rackがファイルアップロード時の一時ファイルを保存するのは Dir.tmpdir で得られるディレクトリということでした。Dir.tmpdirを実行してみたところ、たしかにカレントディレクトリが取得できました。ENV['TMPDIR']を"/tmp"にしても変わらずカレントディレクトリのままでした。

原因

そこで Rubyソースコードを確認してみたところ、原因がわかりました。Dir.tmpdirは以下のようになっています

  def self.tmpdir
    if $SAFE > 0
      @@systmpdir.dup
    else
      tmp = nil
      [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp', '.'].each do |dir|
        next if !dir
        dir = File.expand_path(dir)
        if stat = File.stat(dir) and stat.directory? and stat.writable? and
            (!stat.world_writable? or stat.sticky?)
          tmp = dir
          break
        end rescue nil
      end
      raise ArgumentError, "could not find a temporary directory" unless tmp
      tmp
    end
  end

https://github.com/ruby/ruby/blob/1fae154c07b957278fd336b54256d5c57f21e0d5/lib/tmpdir.rb#L26

問題はこの条件文です。

(!stat.world_writable? or stat.sticky?)

tmpdir候補のディレクトリはこのチェックを突破しないとtmpdirになることができないようです。VolumeMountされたディレクトリを確認したところ、world_writable?はtrueかつ、stickyビットは立っていないことが確認できたので、これが原因で間違いなさそうです。

対応

VolumeMountするディレクトリのパーミッションを変更する方法を調べたのですが見つかりませんでした。

Rubyはどのユーザーでも書き込みできるディレクトリの場合はstickyビットを求めているのに対し、k8sはコンテナ前提なので複数のユーザーがいることはあまり想定していないのでしょう。

仕方ないので、RubyのDir.tmpdirを書き換えてしまいます

class Dir
  class << self
    alias org_tmpdir tmpdir
  end

  def self.tmpdir
    dir = ENV['TMPDIR']
    return File.expand_path(dir) unless dir.nil?

    org_tmpdir
  end
end

manifestsは以下のようにします。

   containers:
      -  image: rubyapp
          name: app
          env:
          - name: TMPDIR
             value: /tmp
          securityContext:
            readOnlyRootFilesystem: true
          volumeMounts:
            - name: tmp
              mountPath: /tmp
     volumes:
        - name: tmp
          emptyDir: {}  

これで RubyのDir.tmpdirが"/tmp"になり、/tmpには書き込みができるのでファイルアップロードができるようになりました。