Apacheのアクセス制御をちゃんと理解する。



Apacheの設定で

Order deny,allow

とか

Satisfy any

とか、なんだか意味わからん人のために。僕はずっとわかってなかった。

基本

Apacheのアクセス制御には、

  • ホストによる制御 (Order,Allow,Deny)
  • ユーザ認証による制御 (Auth*, Require)

の2通りがある。

Satisfyは、2通りあるアクセス制御の両方を満たす必要があるかどうかを決定する。デフォルトはSatisfy all。Satisfy anyなら、どちらか片方満たせばよい。

Order

Order deny,allow

は、全てのホストからのアクセスを許可する。

Order allow,deny

は、全てのホストからのアクセスを拒否する。

Order deny,allow
Allow from 127.0.0.1

は、全てのホストからのアクセスを許可してしまう。もしローカルからだけ許可したいなら、

Order deny,allow
Deny from all
Allow from 127.0.0.1

と書かなければならない。逆に allow,deny なら、

Order allow,deny
Allow from 127.0.0.1

でローカルからのアクセスだけを許可できる。調子に乗って

Order allow,deny
Deny from all
Allow from 127.0.0.1

などとすると、全てのアクセスを拒否してしまう。


どういう意味?

図にするとこういう処理をしている。

擬似コードの方がわかりやすいかもしれない。

<Location />
  Order deny,allow
</Location>

は、

if (Location.match? /) {
  access = allow // デフォルト
  if (Host.match? none) { // deny条件(なし)
    access = deny
    if (Host.match? none) { // allow条件(なし)
      access = allow
    }
  }
}

という処理をしている。デフォルトの存在に注目。Order deny,allowの場合、まずデフォルトで許可される。次にDeny fromの条件に合うか調べられ、合致したら拒絶に変わる。さらにAllow fromに合致すると許可に戻る。

Order allow,denyの場合は、逆に拒絶がデフォルトになる。

<Location />
  Order allow,deny
  Allow from all
  Deny from spam.example.com
</Location>

は、

if (Location.match? /) {
  access = deny // デフォルト
  if (Host.match? all) { // allow条件
    access = allow
    if (Host.match? spam.exaple.com) { // deny条件
      access = deny
    }
  }
}

という処理になり、spam.example.com 以外のホストが許可される。

  • Order (allow,)deny,allow
  • Order (deny,)allow,deny

のように先頭に省略があると考えるとわかりやすい。よく見かけるのは、

Order deny,allow
Deny from all
Allow from xxx

というパターン。これは、Allowにマッチしたホストだけを許可しますよという意味。

Order allow,deny
Allow from xxx

と同じだが、全てを拒絶する(Deny from all)と明示的に書いた方がわかりやすい。

なお、if文が入れ子になっていると考える必要はない。Order deny,allow は、

if (Location.match? /) {
  access = allow
  if (Host.match? none) access = deny
  if (Host.match? none) access = allow
}

という擬似コードの方が適当かもしれない。ただ、自分は入れ子になっている方が理解しやすかった。denyでないなら下の判定は不要だし。

マージ

ここまで挙げた例には <Location /> が1つしか出てこなかった。複数の Location セクションがあるとどうなるか。

マニュアルでは、

でページを割いて解説している。

  • Directory以外は、設定ファイルに現れた順番に処理される。
  • Directoryは短い方から長い方に並べ替えてから処理される。
    • 複数のDirectoryが同じディレクトリに適用される場合は、設定ファイルの順に処理される。

ということなのだが、「処理される」の内容がわからない。

次のスクリプトを書いて、設定を変えながら実験した。ドキュメントルートに index.html と 1 というファイルが置いてある。

#! /bin/sh

/opt/local/apache2/bin/apachectl restart

echo /
curl -s -I http://default.local/  | head -n1 
echo /1
curl -s -I http://default.local/1 | head -n1 

まず、Locationの中に何も書かなかった場合。

<Location />
  Order allow,deny
  Allow from all
  Deny from all
</Location>

<Location /1>
</Location>

これは / も /1 も拒絶される。何も書かない場合、デフォルトで上書きされたりはしない。

/
HTTP/1.1 403 Forbidden
/1
HTTP/1.1 403 Forbidden


次に Order だけを省略するとどうなるか。

<Location />
  Order allow,deny
  Allow from all
  Deny from all
</Location>

<Location /1>
  Allow from all
  Deny from all
</Location>

これは / は拒絶され、/1 は許可される。

/
HTTP/1.1 403 Forbidden
/1
HTTP/1.1 200 OK

/ も /1 も同じように Allow from all,Deny from all していることに注意。/1だけが許可されるのは、デフォルトの Order deny,allow が適用されるため。擬似コードだと、

if (Location.match? /) {
  access = deny
  if (Host.match? all) {
    access = allow
    if (Host.match? all) {
      access = deny // 全てのアクセスがまず拒絶される。
    }
  }
}

if (Location.match? /1) { 
  access = allow // デフォルトの Order deny,allow
  if (Host.match? all) {
    access = deny
    if (Host.match? all) {
      access = allow // /1はこれが適用される。
    }
  }
}

となる。Order,Allow,Deny は3つで1セット(mod_authz_host)で、どれか1つを書くとデフォルトが有効になるようだ。

Location は設定ファイル内の順序に影響を受ける。上の設定の順序だけを入れ替えると結果が変わる。

<Location /1>
  Allow from all
  Deny from all
</Location>

<Location />
  Order allow,deny
  Allow from all
  Deny from all
</Location>
/
HTTP/1.1 403 Forbidden
/1
HTTP/1.1 403 Forbidden

これはif文が入れ替わるため。

if (Location.match? /1) { 
  access = allow // Order deny,allow
  if (Host.match? all) {
    access = deny
    if (Host.match? all) {
      access = allow // /1を許可するが、、
    }
  }
}

if (Location.match? /) {
  access = deny
  if (Host.match? all) {
    access = allow
    if (Host.match? all) {
      access = deny // 結局全部拒否されてしまう。
    }
  }
}

Satisfy Any

ユーザ認証と併用するとどうなるか。

<Location />
  Order deny,allow
  Deny from all
  Allow from 127.0.0.1

  AuthType Basic
  AuthName "Staff Only"
  AuthUserFile /path/to/pass
  Require valid-user

  Satisfy any
</Location>

<Location /1>
  Order deny,allow
  Deny from all
</Location>
/
HTTP/1.1 200 OK
/1
HTTP/1.1 401 Authorization Required

Satisfy any の効果で、ローカルから認証なしで / にアクセスできる。ここまでは良い。 /1 の方が問題。明示的にアクセスを禁止しているのに、なぜか認証を求められる。認証が通るとアクセスできてしまう。

この挙動は、次のような擬似コードを考えると、理解できる。

// デフォルトは何も制限されない。
access     = allow;
hostAccess = allow;
authAccess = allow;

// /のホスト制限。
if (Location.match? /) {
  hostAccess = allow;
  if (Host.match? all) {
    hostAccess = deny;
    if (Host.match? 127.0.0.1) {
      hostAccess = allow;
    }
  }
}

// Basic認証。
if (Location.match? /) {
  authAccess = deny;
  if (User.match? valid-user) {
    authAccess = allow;
  }
}

// /1のホスト制限。
if (Location.match? /1) {
  hostAccess = allow;
  if (Host.match? all) {
    hostAccess = deny;
    if (Host.match? none) {
      hostAccess = allow;
    }
  }
}

// 最後にSatisfy anyの判定を行う。
if (Location.match? /) {
  access = deny;
  if (hostAccess || authAccess) {
    access = allow;
  }
}

/1 は Location / に合致するので、Basic認証が適用され、さらに Satisfy any も適用される。

というわけで、/1 には Satisfy all を書くのが正解。

<Location />
  Order deny,allow
  Deny from all
  Allow from 127.0.0.1

  AuthType Basic
  AuthName "Staff Only"
  AuthUserFile /path/to/pass
  Require valid-user

  Satisfy any
</Location>

<Location /1>
  Order deny,allow
  Deny from all

  Satisfy all
</Location>

これで、ホストの条件が必須になり、/1 へのアクセスが全て拒否される。

/
HTTP/1.1 200 OK
/1
HTTP/1.1 403 Forbidden

Directory

力尽きました。短いのから長いのの順に並べ替えてから処理されるとのこと。

念のため書くと上のような例では Location ではなく Directory/Files を使うべき。

まとめ

こんなバッドノウハウ、本当はどうでもいいと思う。Apache 3.0では、かっこいいDSL(VCL)で書けるようにする構想があるらしいのでがんばってほしい。

可読性と複雑さの両立には DSL が向いている。設定の中に制御構造が含まれるような場合は特に DSL がよい。自分自身を説明しないボキャブラリは独善で邪悪です。