MacからXserverの本番環境へ、rsyncでWordPressのファイル一式を転送したときのことです。
サイトのトップページも記事ページも問題なく表示されるのに、管理画面にアクセスすると403 Forbidden。ログイン画面すら出てきません。
原因は、rsyncの -a オプションがMacのファイル権限をそのまま本番に持ち込んでいたことでした。macOSのホームディレクトリ配下は「本人しか読めない」設定になっているため、それが転写されるとWebサーバーがファイルを読めなくなります。
なお、この作業に至る前段として、ローカル環境をMAMPからDDEVへ移行しています。
その過程でも別のエラーに遭遇しました。

何が起きたか
作業内容はシンプルです。ローカルで検証を済ませたWordPress一式を、rsyncで本番に転送しました。
rsync -avz --delete \
--exclude 'wp-config.php' \
--exclude 'wp-content/uploads' \
/path/to/local/ \
server:path/to/production/転送は正常に完了。サイトを開くと、トップページも記事ページもきちんと表示されます。
ところが管理画面にアクセスすると403が返ってきました。
403 Forbidden404ならファイルが存在しない、500ならPHPのエラー。403は「ファイルはあるがアクセスを拒否された」という意味です。
何を疑ったか
最初に思い浮かんだのは2つでした。
ひとつは .htaccess です。アクセス制限を書くファイルなので、403と聞けばまずここを疑います。ただ、今回の転送では .htaccess を除外していたはずでした。
もうひとつは、上書きしてはいけないファイルを上書きしてしまった可能性です。wp-config.php は除外していましたが、他にサーバー固有の設定ファイルがあって、それを壊したのではないか。
どちらもハズレでした。
原因はパーミッションだった
管理画面のファイルを確認してみます。該当のディレクトリに移動して、権限を見ます。
ls -la index.php
ls -ld .結果です。
-rw------- 1 user group 7864 index.php
drwx------ 9 user group 4096 .これが原因でした。
恥ずかしながら、この表示を見てもすぐにはピンときませんでした。パーミッションはいつも644や755といった数字で扱っていて、drwx------ のような記号表記に慣れていなかったからです。たまに記号表記があっても見て見ぬ振りをしてまして…。
記号表記の読み方
記号は3文字ずつ、3つのブロックに分かれています。
d rwx ------
│ │ │
│ │ └─ グループ・その他ユーザーの権限(ここでは何もなし)
│ └───── 所有者の権限(読み・書き・実行すべて可)
└─────── ディレクトリを示す(ファイルなら - )数字との対応はこうなります。
| 記号 | 数字 | 意味 |
|---|---|---|
rwxr-xr-x | 755 | 所有者は全部、他は読み取りと実行 |
rw-r--r-- | 644 | 所有者は読み書き、他は読み取りのみ |
drwx------ | 700 | 所有者だけ。他は何もできない |
-rw------- | 600 | 所有者だけ読み書き。他は読めない |
つまり今回の状態は、所有者以外はファイルを一切読めないというものでした。
Webサーバーは通常、ファイルの所有者とは別のユーザーとして動きます。読み取り権限がなければファイルを配信できず、403を返す。当然の結果です。
なぜ権限が変わったのか
犯人は rsync -a の a です。
-a(archive)は複数のオプションをまとめた指定で、その中に -p(permissions)が含まれています。これは「送り元のパーミッションを送り先にも反映する」という意味です。
そして、macOSのホームディレクトリ配下は、初期状態で厳しい権限になっています。~/Sites などに置いたファイルは、多くの場合700や600です。本人以外がアクセスできないようになっているわけです。
この権限がそのまま本番サーバーに転写された結果、Webサーバーから読めないファイル群ができあがりました。
サイトのトップページが表示されていたのは、キャッシュか、あるいは一部のファイルがたまたま読める状態だったためだと思われます。
対処法:転送時に権限を指定する
rsyncには、権限の扱いを制御するオプションがあります。
rsync -avz --delete --no-perms --chmod=D755,F644 \
/path/to/local/ \
server:path/to/production/--no-perms— 送り元のパーミッションを反映しない--chmod=D755,F644— ディレクトリを755、ファイルを644に設定する
D がディレクトリ、F がファイルを表します。WordPressの一般的な推奨値です。
すでに壊れている場合
転送済みで403になっているなら、サーバー側でまとめて直せます。
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;ディレクトリとファイルを分けて処理します。ただし wp-config.php だけは、DB接続情報が入っているので厳しくしておくのが安全です。
chmod 600 wp-config.phpところが --chmod でエラーが出る
対処法が分かったので、さっそくオプションを付けて実行しました。
rsync: --chmod=D755,F644: invalid argument無効な引数だと言われます。
正しい書き方をしているはずなのに、なぜなんだ…。
そもそも私はこのときrsyncというコマンドをよく知らず、Macに最初から入っていることすら意識していませんでした。「わけのわからないエラーが出たな」というのが率直な感想です。
バージョンを確認してみます。
rsync --versionopenrsync: protocol version 29
rsync version 2.6.9 compatible2.6.9。 調べてみると、2006年のバージョンでした。ちなみに使っているmacは2025年製です。
macOSに同梱されているrsyncは長らく更新されておらず、--chmod を含む新しめのオプションに対応していません。ライセンスの都合で古いバージョンが据え置かれているようです。
新しいrsyncを入れる
Homebrewで導入します。
brew install rsyncインストール後、バージョンを確認しても古いまま表示されることがあります。
rsync --version
# rsync version 2.6.9 compatibleこれはシェルがコマンドの場所をキャッシュしているためです。キャッシュを消すと反映されます。
hash -r
rsync --versionrsync version 3.5.0 protocol version 32新しいターミナルを開き直しても同じ効果があります。
なお、Homebrew版が優先されているかは次のコマンドで確認できます。
which -a rsync/opt/homebrew/bin/rsync
/usr/bin/rsyncHomebrew版が上にあれば正しく設定されています。
再発防止:デプロイ用のスクリプトを作る
毎回オプションを手で打つと、いつか忘れます。
rsyncの実行前に確認する仕組みが必要だと感じました。
そこで、確認を挟むラッパースクリプトを用意しました。
#!/bin/bash
set -uo pipefail
# 安全なrsyncデプロイ
# 使い方: safe-deploy.sh <ローカルの絶対パス> <接続名:リモートパス>
RED='\033[0;31m'; YEL='\033[0;33m'; GRN='\033[0;32m'; NC='\033[0m'
if [ $# -ne 2 ]; then
echo "使い方: $0 <ローカル絶対パス> <接続名:リモートパス>"
exit 1
fi
SRC="$1"
DEST="$2"
# --- 検証1: 絶対パスか ---
case "$SRC" in
/*) ;;
*) echo -e "${RED}エラー: 送り元は絶対パスで指定してください${NC}"; exit 1 ;;
esac
# --- 検証2: 危険な送り元でないか ---
case "$SRC" in
"$HOME"|"$HOME/"|/|/Users|/Users/)
echo -e "${RED}エラー: ホームディレクトリやルートは送り元にできません${NC}"
exit 1 ;;
esac
# --- 検証3: WordPressらしきディレクトリか ---
if [ ! -d "$SRC/wp-content" ]; then
echo -e "${YEL}警告: $SRC に wp-content がありません${NC}"
read -p "続行しますか? (yes/no): " ok
[ "$ok" = "yes" ] || exit 1
fi
EXCLUDES=(
--exclude '.ddev'
--exclude '.git'
--exclude 'wp-config.php'
--exclude '.htaccess'
--exclude '.user.ini'
--exclude 'robots.txt'
--exclude '*.sql'
--exclude '*.sql.gz'
--exclude 'wp-content/uploads'
--exclude '.DS_Store'
--exclude 'node_modules'
)
OPTS=(-avz --delete --no-perms --chmod=D755,F644)
echo -e "${GRN}=== DRY RUN ===${NC}"
echo "送り元: $SRC/"
echo "送り先: $DEST/"
echo ""
OUT=$(rsync "${OPTS[@]}" -n "${EXCLUDES[@]}" "$SRC/" "$DEST/" 2>&1)
RC=$?
if [ $RC -ne 0 ]; then
echo -e "${RED}dry-runが失敗しました(終了コード: $RC)${NC}"
echo "$OUT"
exit 1
fi
echo "$OUT" | grep -E "^Transfer starting" || echo "(転送ファイル数の表示なし)"
# 削除リスト
DEL=$(printf '%s\n' "$OUT" | grep "^deleting" || printf '')
if [ -z "$DEL" ]; then
DELCOUNT=0
else
DELCOUNT=$(printf '%s\n' "$DEL" | wc -l | tr -d ' ')
fi
echo "削除予定: ${DELCOUNT} 件"
# 危険パターン検出
DANGER=$(printf '%s\n' "$DEL" | grep -E "uploads/|\.htaccess|\.user\.ini|robots\.txt|favicon\.ico" || printf '')
if [ -n "$DANGER" ]; then
echo -e "${RED}"
echo "!!! 危険: 以下が削除対象に含まれています !!!"
echo "$DANGER"
echo -e "${NC}"
echo "中止します。除外設定を見直してください。"
exit 1
fi
echo ""
echo "--- 削除予定(先頭20件)---"
echo "$DEL" | head -20
echo ""
echo -e "${YEL}上記の内容で本番に反映します。${NC}"
read -p "実行しますか? (yes/no): " ok
[ "$ok" = "yes" ] || { echo "中止しました"; exit 0; }
echo -e "${GRN}=== 実行中 ===${NC}"
if rsync "${OPTS[@]}" "${EXCLUDES[@]}" "$SRC/" "$DEST/"; then
echo -e "${GRN}完了しました${NC}"
else
echo -e "${RED}エラーで終了しました${NC}"
exit 1
fiやっていることは4つです。
- 権限オプションを常に付ける(今回の本題)
- 送り元が絶対パスかを検証する
- 必ずdry-runを先に実行し、削除されるファイルを表示する
- 消えると困るファイルが削除対象に入っていたら中止する
~/bin などパスの通った場所に置いて、実行権限を付ければ使えます。
chmod +x ~/bin/safe-deploy.sh
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrcまとめ
rsync -aは送り元のパーミッションを転送先に反映する- macOSのホーム配下は700や600になっていることが多く、それが転写されるとWebサーバーが読めない
- 結果として、サイトは見えるのに管理画面だけ403、といった状態になる
--no-perms --chmod=D755,F644を付ければ防げる- ただしmacOS標準のrsyncは2.6.9(2006年)で
--chmodに非対応。brew install rsyncで3.x系を入れる必要がある - インストール後に古いバージョンが表示されるときは
hash -r
403は「ファイルはあるがアクセスできない」という意味なので、パーミッションを疑うのが定石でした。ただ普段644や755という数字で扱っていると、drwx------ という記号表記から即座に「権限が厳しすぎる」と読み取るのは意外と難しいものです。
MacからLinuxサーバーへrsyncでデプロイしている方は、一度 ls -la で本番の権限を確認してみるとよいかもしれません。
また、同じアップグレード作業では、PHPのバージョンを上げたことで
OGP画像の生成が壊れる問題も起きています。


