
scpコマンドとは、SSHの接続の上でマシン間のファイルをコピーするOpenSSHのコマンドで、OpenSSH 9.0からは、既定で転送にSFTPプロトコルを使います。差分だけを送る同期や、中断した転送の再開をする道具ではありません。Ubuntu 26.04.1 の OpenSSH 10.2p1 を検証用のコンテナ3つで動かし、コピー先の名前・権限・時刻の決まり方、-rでのシンボリックリンクの扱い、同じコマンドが既定のモードと旧プロトコル(-O)で別の結果になる場面を、実際の出力で確かめました。
対象は、設定ファイルやログ、バックアップをサーバーへ運ぶのにscpを使っている情シス・インフラ担当の方です。読み終えると、手元のscpがどちらのプロトコルで動いているかを確かめるコマンド、権限と時刻を元のまま運ぶ指定、踏み台越しとサーバー同士のコピー、スクリプトに組み込むときの失敗の拾い方、そしてscp・sftp・rsyncを選ぶ基準が手元に残ります。
この記事でわかること
- 手元の
scpがSFTPと旧SCPプロトコルのどちらで動いているかを-vで確かめる方法と、-Oを付けると何が変わるか - コピー先の名前・権限・時刻の決まり方(
-pの有無、手元と相手のumask、既存ファイルへの上書き) -rでシンボリックリンクが実体のコピーになること、同じコマンドをもう一度打つと入れ子になる条件-P・-i・~/.ssh/config・-J・サーバー同士のコピー(-3と-R)の実行結果と、-l・-Cの測定値- 中断した転送の続け方と続けてはいけない場合、スクリプトでの終了コードの扱い、scp・sftp・rsyncの使い分け
検証環境:Ubuntu 26.04のコンテナ3つとOpenSSH 10.2p1
検証はDockerのubuntu:26.04コンテナ3つで行いました。コピーを実行する側(以下クライアント)のホスト名は nagoyadevlab01、相手のサーバーは nagoyadevlab02、踏み台とサーバー同士のコピーの相手に使う3つ目は nagoyadevlab03、作業ユーザーはどれも ryoma です。すべて検証用に付けた名前なので、コマンドや出力に出てくるryoma、/home/ryoma、nagoyadevlab01からnagoyadevlab03、コンテナとネットワークの名前scp-command-usage-options-…、設定ファイルに書く別名のlab03とlab02-alt、ssh-ed25519 AAAA…の公開鍵、172.18.0.2などのアドレスは、自分の環境の値に読み替えてください。コンテナは--privilegedなし・ポート公開なしで、1つのユーザー定義ネットワークに置きました。Dockerを動かすホストで実行したのは次のコマンドです(docker network createとdocker runが表示する長いIDは省いています)。
docker network create scp-command-usage-options-net
docker run -d --init --name scp-command-usage-options-lab01 --hostname nagoyadevlab01 --network scp-command-usage-options-net --network-alias nagoyadevlab01 --memory 512m ubuntu:26.04 sleep infinity
docker run -d --init --name scp-command-usage-options-lab02 --hostname nagoyadevlab02 --network scp-command-usage-options-net --network-alias nagoyadevlab02 --memory 512m ubuntu:26.04 sleep infinity
docker run -d --init --name scp-command-usage-options-lab03 --hostname nagoyadevlab03 --network scp-command-usage-options-net --network-alias nagoyadevlab03 --memory 512m ubuntu:26.04 sleep infinity
docker ps --filter network=scp-command-usage-options-net --format '{{.Names}}'
scp-command-usage-options-lab03
scp-command-usage-options-lab02
scp-command-usage-options-lab01ここから先のコマンドは、コンテナの中のshに1つずつ渡して実行し、標準出力と標準エラー出力を画面に出る順のまま記録したものです。クライアントの作業ユーザーならdocker exec -u ryoma -w /home/ryoma/work scp-command-usage-options-lab01 sh -lc 'コマンド'の形になります。端末(TTY)を割り当てていないので、scpの進捗表示は出ません。端末がある場合との違いは、最初のコピーのところで一度だけ示します。
コンテナの中ではsystemdが動かないため、sshdはバイナリを直接起動しました。実機のUbuntuではsystemdのユニットがsshdを起動していて、待ち受けポートを増やす手順や設定の反映のしかたがここと違います。systemctlはこの環境では実行していません。この記事で確かめるのはscpを打つ側の挙動なので、サーバー側は「鍵認証のみ・22番と2222番で待ち受け」という状態だけを作っています。
接続する側には、SSHクライアントと、後半の比較で使うrsyncを入れます。root で実行しました。
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-client rsync >/dev/null 2>&1; echo "exit=$?"
exit=0
useradd -m -s /bin/bash ryoma && id ryoma
uid=1001(ryoma) gid=1001(ryoma) groups=1001(ryoma)
grep PRETTY_NAME /etc/os-release; ssh -V; dpkg-query -W openssh-client rsync
PRETTY_NAME="Ubuntu 26.04.1 LTS"
OpenSSH_10.2p1 Ubuntu-2ubuntu3.7, OpenSSL 3.5.5 27 Jan 2026
openssh-client 1:10.2p1-2ubuntu3.7
rsync 3.4.1+ds1-7ubuntu0.3サーバー側も root で準備します。60-lab.confに書いたPasswordAuthentication noはパスワード認証を止める設定、Portの2つは22番に加えて2222番でも待ち受ける設定で、2222番は-Pの検証のためだけに足しました。
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server rsync >/dev/null 2>&1; echo "exit=$?"
exit=0
useradd -m -s /bin/bash ryoma && id ryoma
uid=1001(ryoma) gid=1001(ryoma) groups=1001(ryoma)
printf 'PasswordAuthentication no\nPort 22\nPort 2222\n' > /etc/ssh/sshd_config.d/60-lab.conf && /usr/sbin/sshd -t && /usr/sbin/sshd; echo "exit=$?"
exit=0
sshd -V; dpkg-query -W openssh-server openssh-sftp-server rsync; sshd -T | grep -E '^(port|passwordauthentication|subsystem) '
OpenSSH_10.2p1 Ubuntu-2ubuntu3.7, OpenSSL 3.5.5 27 Jan 2026
openssh-server 1:10.2p1-2ubuntu3.7
openssh-sftp-server 1:10.2p1-2ubuntu3.7
rsync 3.4.1+ds1-7ubuntu0.3
port 22
port 2222
passwordauthentication no
subsystem sftp /usr/lib/openssh/sftp-server
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:oS/mi9aR4c0TKGJw+UU1qSWh5xNSdELHPPfUHM+xSk8 root@nagoyadevlab02 (ED25519)sshd -Tの最後の行subsystem sftp /usr/lib/openssh/sftp-serverが、既定のscpが相手側で使うSFTPサーバーです。その下のSHA256:で始まる値はサーバーのホスト鍵の指紋で、すぐあとでクライアント側の表示と見比べます。3つ目のマシンにも同じ準備をします。こちらは22番だけです。
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server >/dev/null 2>&1; echo "exit=$?"
exit=0
useradd -m -s /bin/bash ryoma && id ryoma
uid=1001(ryoma) gid=1001(ryoma) groups=1001(ryoma)
printf 'PasswordAuthentication no\n' > /etc/ssh/sshd_config.d/60-lab.conf && /usr/sbin/sshd -t && /usr/sbin/sshd; echo "exit=$?"
exit=0
sshd -V; ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
OpenSSH_10.2p1 Ubuntu-2ubuntu3.7, OpenSSL 3.5.5 27 Jan 2026
256 SHA256:fJ7iLpJQm56C/2SiXmqwjFh1d1CZRfplks/lRboZts4 root@nagoyadevlab03 (ED25519)作業ユーザーに切り替え、クライアントで鍵を作ります。-N ""でパスフレーズを空にしたのは検証を非対話で回すためで、人が日常的に使う鍵にはパスフレーズを付けます(鍵の種類やssh-agentの扱いはssh-keygenでSSH鍵を作る記事にまとめています)。
ssh-keygen -q -t ed25519 -N "" -C "ryoma@nagoyadevlab01" -f ~/.ssh/id_ed25519 && cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDqMqWD8L/7d7dZfFXnOcYBHNl02AOkd/mlT3TbOh7AY ryoma@nagoyadevlab01表示された公開鍵を、サーバーの同じユーザーの~/.ssh/authorized_keysに追記します。ssh-ed25519 AAAA…の部分は、自分の画面に出たものに置き換えてください。
install -d -m 700 ~/.ssh && echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDqMqWD8L/7d7dZfFXnOcYBHNl02AOkd/mlT3TbOh7AY ryoma@nagoyadevlab01" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys手元のクライアントに戻り、サーバーのホスト鍵をknown_hostsに登録します。表示された指紋が、さきほどサーバーで表示したSHA256:以降と同じであることを目で確かめてから、最初の接続をします。
ssh-keyscan -t ed25519 nagoyadevlab02 | tee -a ~/.ssh/known_hosts | ssh-keygen -lf -
256 SHA256:oS/mi9aR4c0TKGJw+UU1qSWh5xNSdELHPPfUHM+xSk8 nagoyadevlab02 (ED25519)
ssh ryoma@nagoyadevlab02 'hostname; umask'
nagoyadevlab02
0002umaskの0002は、このサーバーでこのユーザーのセッションに付いているumaskです。権限の節で、届いたファイルの権限がこの値のとおりに削られることを確かめます。
scpコマンドの基本の形と、中身がSFTPであることの確かめ方
書式はscp [オプション] コピー元 コピー先です。リモート側はユーザー名@ホスト名:パスと書き、コロンの後ろが/で始まらなければ、相手のホームディレクトリからの相対パスとして扱われました。検証用のファイルを2つ作ります。report.csvは権限640・更新時刻を2026年1月15日に、deploy.shは権限と時刻の変化が見えやすいように、わざと777・2月1日にしてあります(777は検証のための値で、実運用のスクリプトに付ける権限ではありません)。
mkdir ~/work && cd ~/work && printf 'date,host,status\n2026-10-01,web01,ok\n2026-10-02,web01,ok\n' > report.csv && chmod 640 report.csv && touch -d '2026-01-15 09:30:00' report.csvここから先、クライアントのコマンドは~/workの中で実行しています。サーバーには受け取り用のinboxを作っておきます。
printf '#!/bin/sh\necho deploy\n' > deploy.sh && chmod 777 deploy.sh && touch -d '2026-02-01 12:00:00' deploy.sh
umask; stat -c '%a %s %.19y %n' report.csv deploy.sh
0022
640 57 2026-01-15 09:30:00 report.csv
777 22 2026-02-01 12:00:00 deploy.sh
ssh ryoma@nagoyadevlab02 'mkdir inbox'手元からサーバーへ送るのが次の形です。
scp report.csv ryoma@nagoyadevlab02:inbox/; echo "exit=$?"
exit=0何も表示されずにexit=0で終わりました。端末を割り当てていない記録なので、進捗の行が出ていません。同じコマンドをscriptの擬似端末の中から実行すると、進捗が表示されます。
script -qc 'scp report.csv ryoma@nagoyadevlab02:inbox/' /dev/null
report.csv 0% 0 0.0KB/s --:-- ETA
report.csv 100% 57 269.9KB/s 00:00進捗は同じ位置に上書きされていく表示なので、記録には0%の時点と100%の時点の2つが残りました。ファイル名、割合、バイト数、速度、時間の順です。パスワード認証のサーバーが相手なら、scpは必要に応じてパスワードやパスフレーズを尋ねるとマニュアルにあります。この検証は鍵認証のみにしているので、尋ねられる場面はありません。
サーバーから手元へ持ってくるときは、コピー元とコピー先を入れ替えます。
mkdir got && scp ryoma@nagoyadevlab02:inbox/report.csv got/; echo "exit=$?"; ls -l got
exit=0
total 4
-rw-r----- 1 ryoma ryoma 57 Oct 13 07:43 report.csvどちらのプロトコルで運ばれたかは、-vのデバッグ出力から「相手に何を要求したか」の行を抜き出すとわかります。
scp -v report.csv ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E 'Sending (subsystem|command)'
debug1: Sending subsystem: sftp
scp -O -v report.csv ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E 'Sending (subsystem|command)'
debug1: Sending command: scp -v -t inbox/既定ではSending subsystem: sftp、つまり相手のsshdにSFTPサーバーを起動させています。-Oを付けるとSending command: scp -v -t inbox/に変わり、相手側でもscpコマンドを起動させる旧来の方式になりました。
OpenSSHのリリースノートによると、scpが既定でSFTPを使うようになったのは9.0(2022年4月8日公開)で、旧プロトコルに戻す-Oも同じ箇所に書かれています。scp(1)のマニュアルは-Oが要る場面として、SFTPを実装していないサーバー、特定のワイルドカードの後方互換、古いSFTPサーバーでの~で始まるパスの展開の3つを挙げています。リリースノートの記述どおりなら9.0より前の版は旧プロトコルが既定のはずですが、古い版はこの環境では動かしていません。自分の版はssh -Vで確かめられます。
scpのオプション早見表:この記事で実行したもの
この記事で実際に実行したオプションを、確かめた結果と一緒に並べます。「効果」はscp(1)のマニュアルの説明、「実行結果」はOpenSSH 10.2p1の既定のモードで見たものです。
| オプション | 効果 | 実行結果 |
|---|---|---|
-r | ディレクトリを中身ごとコピーする | シンボリックリンクは実体のコピーになった |
-p | 更新時刻・アクセス時刻・権限を元のままにする | umaskに関係なく777のまま届き、更新時刻も元の値だった |
-P ポート | 接続先のポートを指定する | 小文字の-p 2222はexit=255で、何も送られなかった |
-i 鍵ファイル | 認証に使う秘密鍵を指定する | 既定の名前でない鍵は、指定するまで使われなかった |
-J 踏み台 | 踏み台を経由して接続する | サーバーから見た接続元が踏み台のアドレスになった |
-3 | サーバー同士のコピーを手元経由で行う(既定) | 付けても付けなくても成功した |
-R | サーバー同士のコピーを、コピー元から直接行う | コピー元のサーバーに相手のホスト鍵がなく失敗した |
-l 上限 | 帯域の上限をKbit/sで指定する | -l 16000でin 11.2 seconds |
-C | 圧縮を有効にする | 送信量は減ったが、同じホスト上のコンテナ間では時間が増えた |
-O | 旧SCPプロトコルで転送する | 空白・波かっこ・$HOMEを含むパスの結果が変わった |
-v | デバッグ出力を表示する | プロトコルの確認と送受信バイト数の確認に使った |
-q | 進捗と、sshの警告・診断メッセージを止める | 認証に失敗した理由の行まで消えた |
-o 項目=値 | ssh_configの項目を直接指定する | BatchMode=yesとConnectTimeout=10を使った |
コピー先の名前はどう決まるか
宛先にファイル名まで書けばその名前になり、ディレクトリを書けばその中に元と同じ名前で置かれます。次の4つは順に、inboxの中へ別名で、ホームディレクトリ(コロンだけ)へ同じ名前で、絶対パスの/tmp/へ同じ名前で、最後は末尾の/を付けないinboxへ送っています。
scp report.csv ryoma@nagoyadevlab02:inbox/renamed.csv; echo "exit=$?"
exit=0
scp report.csv ryoma@nagoyadevlab02:; echo "exit=$?"
exit=0
scp report.csv ryoma@nagoyadevlab02:/tmp/; echo "exit=$?"
exit=0
scp report.csv ryoma@nagoyadevlab02:inbox; echo "exit=$?"
exit=0最後の例のように、すでにあるディレクトリが宛先なら、末尾の/がなくてもその中に入ります(すぐあとのstatで、inboxがディレクトリのままであることを確かめます)。気をつけたいのは、まだ存在しないディレクトリを宛先に書いた場合です。末尾に/を付けたときはエラーで止まりました。
scp report.csv ryoma@nagoyadevlab02:archive/; echo "exit=$?"
scp: dest open "archive/": Failure
scp: failed to upload file report.csv to archive/
exit=1/を付けずに同じことをすると、エラーにならず、archiveという名前の通常ファイルができます。
scp report.csv ryoma@nagoyadevlab02:archive; echo "exit=$?"
exit=0
ssh ryoma@nagoyadevlab02 "stat -c '%F %s %n' inbox inbox/report.csv inbox/renamed.csv report.csv /tmp/report.csv archive"
directory 4096 inbox
regular file 57 inbox/report.csv
regular file 57 inbox/renamed.csv
regular file 57 report.csv
regular file 57 /tmp/report.csv
regular file 57 archive最後の行のregular file 57 archiveがそれで、中身はreport.csvです。ファイルを1つ送ったこの2つの例では、scpがarchiveというディレクトリを作ることはありませんでした。ファイルを入れるディレクトリは、先にssh ユーザー名@ホスト名 'mkdir -p ディレクトリ'で作っておきます。まちがってできたファイルは消しておきます。
ssh ryoma@nagoyadevlab02 'rm archive'宛先をディレクトリのつもりで書くときは、末尾の/を省かないでください。ファイルを1つ送る場合、付けておけば、ディレクトリがなかったときにdest open "archive/": Failureで止まります。省くと、ディレクトリがあればその中に入り、なければエラーにならないまま同じ名前の通常ファイルが作られました。同じコマンドの結果が相手側の状態で変わるので、送り先のまちがいに気づくのが遅れます。
権限と時刻:-pなしはumask任せ、-pで元のまま
権限777・更新時刻2月1日のdeploy.shを、-pなしと-pありで送り、サーバー側の権限・サイズ・更新時刻を並べます。
scp deploy.sh ryoma@nagoyadevlab02:inbox/; echo "exit=$?"
exit=0
scp -p deploy.sh ryoma@nagoyadevlab02:inbox/deploy-p.sh; echo "exit=$?"
exit=0
ssh ryoma@nagoyadevlab02 "stat -c '%a %s %.19y %n' inbox/deploy.sh inbox/deploy-p.sh"
775 22 2026-10-13 07:43:19 inbox/deploy.sh
777 22 2026-02-01 12:00:00 inbox/deploy-p.sh-pなしで送ったdeploy.shは権限775、更新時刻は転送した時刻になりました。サーバー側のumaskが0002なので、777から「その他」の書き込みビットが落ちた値です。-pを付けたdeploy-p.shは777と2月1日のままで、umaskの影響を受けていません。マニュアルの説明では、-pが保つのは更新時刻・アクセス時刻・権限のビットです。
権限640のreport.csvを-pなしで送ったinbox/renamed.csvも見ておきます。
ssh ryoma@nagoyadevlab02 "stat -c '%a %s %.19y %n' inbox/renamed.csv"
640 57 2026-10-13 07:43:16 inbox/renamed.csvこちらは640のままです。umaskの0002で落ちるビットを、元の640が持っていないためです。更新時刻は、こちらも転送した時刻になっています。
逆に、手元へ持ってくる方向では、効くのは手元のumaskです。同じファイルを、手元のumaskが0022のまま、umask 077に変えたサブシェルの中、-p付き、の3通りで受け取ります。
scp ryoma@nagoyadevlab02:inbox/deploy-p.sh got/a.sh; echo "exit=$?"
exit=0
(umask 077; scp ryoma@nagoyadevlab02:inbox/deploy-p.sh got/b.sh; echo "exit=$?")
exit=0
scp -p ryoma@nagoyadevlab02:inbox/deploy-p.sh got/c.sh; echo "exit=$?"
exit=0
stat -c '%a %s %.19y %n' got/a.sh got/b.sh got/c.sh
755 22 2026-10-13 07:43:20 got/a.sh
700 22 2026-10-13 07:43:20 got/b.sh
777 22 2026-02-01 12:00:00 got/c.sh順に755、700、777になりました。-pを付けたc.shだけ、更新時刻も元の2月1日です。
使い分けはこう決めています。成果物やバックアップのように、あとで更新時刻を比べるファイルは-pを付けます。逆に、相手のサーバーで権限を締めたいファイルは-pを付けません。-pは元のファイルの緩い権限もそのまま運ぶからです(上の777がそのまま届いた例)。-pなしで新しく作られたファイルの権限は、ここまでの2例とも「元の権限から相手のumaskのビットを落とした値」でした。秘密にしたいファイルは、送る前に手元で権限を絞っておきます。すでにあるファイルへ上書きする場合は話が変わるので、次の節で確かめます。
既存ファイルへの上書き:権限は残り、長さは最後にそろう
相手側にすでに同じ名前のファイルがあるときの動きを見ます。まずseq 1 1000で作ったlong.txtをinbox/data.txtとして送り、サーバー側で権限を600に変えます。
seq 1 1000 > long.txt; wc -c long.txt report.csv
3893 long.txt
57 report.csv
3950 total
scp long.txt ryoma@nagoyadevlab02:inbox/data.txt; ssh ryoma@nagoyadevlab02 "chmod 600 inbox/data.txt; stat -c '%a %s %n' inbox/data.txt"
600 3893 inbox/data.txtこのdata.txtへ、long.txtより短いreport.csv(権限640)を-pなしで上書きします。-vの出力からはtruncatingを含む行だけを抜き出しました。
scp -v report.csv ryoma@nagoyadevlab02:inbox/data.txt 2>&1 | grep truncating
scp: debug1: truncating at 57
ssh ryoma@nagoyadevlab02 "stat -c '%a %s %n' inbox/data.txt; cat inbox/data.txt"
600 57 inbox/data.txt
date,host,status
2026-10-01,web01,ok
2026-10-02,web01,ok上書き後のサイズはreport.csvと同じで、中身もreport.csvの3つの行だけです。古い内容の残りはありません。権限は600のままで、送った側の640にはなりませんでした。この検証では、-pなしの上書きで変わったのは中身だけで、すでにあるファイルの権限は残っています。
scp: debug1: truncating at 57という行にも注目してください。既定のscpは、宛先を空にしてから書くのではなく、書き終えたあとで宛先を新しい長さに切り詰めています。この順番が結果に出る場面が2つあります。1つは転送が途中で止まったとき(中断の節で実際に壊します)、もう1つは相手のサーバーがSFTPのリクエストを絞っているときです。後者はSFTPの転送専用アカウントを作る記事で確かめていて、fsetstatを断る設定のサーバーへ短いファイルを上書きすると、終了コード0のまま古い内容の残りが付いたファイルになりました。
-pを付けて上書きすると、権限と更新時刻も元のファイルの値に変わります。
scp -p report.csv ryoma@nagoyadevlab02:inbox/data.txt; ssh ryoma@nagoyadevlab02 "stat -c '%a %s %.19y %n' inbox/data.txt"
640 57 2026-01-15 09:30:00 inbox/data.txtディレクトリは-r:シンボリックリンクは実体のコピーになる
検証用のディレクトリを作ります。通常のファイルが2つ、ファイルを指すリンク(home.html)、ディレクトリを指すリンク(styles)、ツリーの外の/etc/hostnameを指すリンク(host-link)が入っています。
mkdir -p site/css && printf '<h1>lab</h1>\n' > site/index.html && printf 'body{margin:0}\n' > site/css/main.css && ln -s index.html site/home.html && ln -s css site/styles && ln -s /etc/hostname site/host-link && ls -lR site
site:
total 8
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 css
lrwxrwxrwx 1 ryoma ryoma 10 Oct 13 07:43 home.html -> index.html
lrwxrwxrwx 1 ryoma ryoma 13 Oct 13 07:43 host-link -> /etc/hostname
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 index.html
lrwxrwxrwx 1 ryoma ryoma 3 Oct 13 07:43 styles -> css
site/css:
total 4
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 main.css-rを付けずに送ると、ディレクトリは通常のファイルではないというエラーで止まります。
scp site ryoma@nagoyadevlab02:inbox/; echo "exit=$?"
scp: local "site" is not a regular file
scp: failed to upload file site to inbox/
exit=1-rを付けて送り、サーバー側を見ます。
scp -r site ryoma@nagoyadevlab02:inbox/; echo "exit=$?"
exit=0
ssh ryoma@nagoyadevlab02 'ls -lR inbox/site; cat inbox/site/host-link'
inbox/site:
total 20
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 css
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 home.html
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 host-link
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 index.html
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 styles
inbox/site/css:
total 4
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 main.css
inbox/site/styles:
total 4
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 main.css
nagoyadevlab01リンクは1つも残っていません。home.htmlは通常のファイルに、stylesは中身の入ったディレクトリになりました。host-linkも通常のファイルで、中身はnagoyadevlab01、つまりクライアントの/etc/hostnameの内容です。ツリーの外を指すリンクがあると、リンク先のファイルの中身まで相手に渡ります。同じことはマニュアルにも書かれていて、-rはたどる途中のシンボリックリンクに従うとあります。旧プロトコルの-Oでも結果は同じでした。
scp -O -r site ryoma@nagoyadevlab02:inbox/site-legacy; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'ls -l inbox/site-legacy'
exit=0
total 20
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 css
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 home.html
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 host-link
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 index.html
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 styles-rで送るディレクトリの中に、鍵や設定ファイルなどツリーの外を指すシンボリックリンクがないかを、送る前にls -lRの->が付いた行で確かめてください。リンクは相手側でリンクにならず、指している先の中身が通常のファイルとしてコピーされます。
もう1つ、同じコマンドを続けて打ったときの違いです。宛先のsite-copyがまだないとき、siteの中身はsite-copyの直下に置かれます。
scp -r site ryoma@nagoyadevlab02:site-copy; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'ls site-copy'
exit=0
css
home.html
host-link
index.html
stylesまったく同じコマンドをもう一度実行すると、今度はsite-copyがすでにあるので、その中にsiteというディレクトリが作られました。
scp -r site ryoma@nagoyadevlab02:site-copy; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'ls site-copy'
exit=0
css
home.html
host-link
index.html
site
styles初回とそれ以降で置き場所が変わるので、同じ宛先へ定期的に送り直す用途にscp -rは向きません。その用途は、末尾の/の有無でコピーの単位が決まり、差分だけを送るrsyncの仕事です(オプションの読み方はrsyncの使い方の記事にあります)。rsync -aで同じsiteを送ると、リンクはリンクのまま届きました。
rsync -a site ryoma@nagoyadevlab02:mirror/; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'ls -l mirror/site'
exit=0
total 8
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 css
lrwxrwxrwx 1 ryoma ryoma 10 Oct 13 07:43 home.html -> index.html
lrwxrwxrwx 1 ryoma ryoma 13 Oct 13 07:43 host-link -> /etc/hostname
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 index.html
lrwxrwxrwx 1 ryoma ryoma 3 Oct 13 07:43 styles -> css手元へ-rで取ってくる方向でも、リンクは実体になります。リンクが残っているmirror/siteを取ってくると、3つのリンクはどれも通常のファイルとディレクトリになり、host-linkの中身は今度はサーバーの/etc/hostnameの内容(nagoyadevlab02)でした。
scp -r ryoma@nagoyadevlab02:mirror/site got/; echo "exit=$?"; ls -l got/site; cat got/site/host-link
exit=0
total 20
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 css
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 home.html
-rw-r--r-- 1 ryoma ryoma 15 Oct 13 07:43 host-link
-rw-r--r-- 1 ryoma ryoma 13 Oct 13 07:43 index.html
drwxr-xr-x 2 ryoma ryoma 4096 Oct 13 07:43 styles
nagoyadevlab02リモート側のワイルドカードと引用符:既定のモードと-Oで結果が変わる
SFTP方式になっていちばん変わったのが、リモート側のパスを誰が解釈するかです。サーバーに、空白入りの名前を含むファイルを3つ置いて比べます。
ssh ryoma@nagoyadevlab02 'mkdir share && cd share && printf "q3\n" > "sales report.txt" && printf "a\n" > a.txt && printf "b\n" > b.txt && ls -l'
total 12
-rw-rw-r-- 1 ryoma ryoma 2 Oct 13 07:43 a.txt
-rw-rw-r-- 1 ryoma ryoma 2 Oct 13 07:43 b.txt
-rw-rw-r-- 1 ryoma ryoma 3 Oct 13 07:43 sales report.txt*を使うときは、リモート側のパス全体を引用符で囲みます。*.txtは既定のモードで3つとも取れ、同じコマンドに-Oを付けてもexit=0でした。3つ目のコマンドは引用符なしですが、この記録のシェル(sh)では同じくexit=0です。手元に一致するファイルがなく、パターンがそのままscpに渡ったためです。手元のシェルが先に展開を試みるかどうかはシェルと設定で変わるので、引用符は付けておきます。
mkdir all && scp 'ryoma@nagoyadevlab02:share/*.txt' all/; echo "exit=$?"; ls all
exit=0
a.txt
b.txt
sales report.txt
scp -O 'ryoma@nagoyadevlab02:share/*.txt' all/; echo "exit=$?"
exit=0
scp ryoma@nagoyadevlab02:share/*.txt all/; echo "exit=$?"
exit=0違いが出るのは、空白を含む名前からです。引用符で囲んだだけのパスは、既定のモードではそのまま取れ、-Oではshare/salesとreport.txtの2つに分かれて失敗しました。
mkdir pull
scp 'ryoma@nagoyadevlab02:share/sales report.txt' pull/; echo "exit=$?"
exit=0
scp -O 'ryoma@nagoyadevlab02:share/sales report.txt' pull/; echo "exit=$?"
scp: share/sales: No such file or directory
scp: report.txt: No such file or directory
exit=1旧プロトコルでは、リモート側のパスを相手のシェルが解釈します。空白をバックスラッシュでエスケープすると-Oでもexit=0になり、同じ書き方は既定のモードでもexit=0でした。
scp -O 'ryoma@nagoyadevlab02:share/sales\ report.txt' pull/; echo "exit=$?"
exit=0
scp 'ryoma@nagoyadevlab02:share/sales\ report.txt' pull/; echo "exit=$?"
exit=0
ls pull
sales report.txt波かっこの展開は逆の結果です。既定のモードでは{a,b}.txtという名前のファイルを探して失敗し、-Oでは相手のシェルが展開して2つとも取れました。
scp 'ryoma@nagoyadevlab02:share/{a,b}.txt' pull/; echo "exit=$?"
scp: share/{a,b}.txt: No such file or directory
exit=1
scp -O 'ryoma@nagoyadevlab02:share/{a,b}.txt' pull/; echo "exit=$?"
exit=0
ls pull
a.txt
b.txt
sales report.txt既定のモードで同じ2つを取るなら、角かっこで書きます。こちらは既定のモードでも-Oでもexit=0です。
scp 'ryoma@nagoyadevlab02:share/[ab].txt' pull/; echo "exit=$?"
exit=0
scp -O 'ryoma@nagoyadevlab02:share/[ab].txt' pull/; echo "exit=$?"
exit=0シェル変数も同じ構図です。$HOMEは既定のモードでは文字どおりのパスとして探されて失敗し、-Oでは相手側で展開されました。~/で始まるパスは、既定のモードでも-Oでもexit=0でした。
scp 'ryoma@nagoyadevlab02:$HOME/share/a.txt' pull/; echo "exit=$?"
scp: $HOME/share/a.txt: No such file or directory
exit=1
scp -O 'ryoma@nagoyadevlab02:$HOME/share/a.txt' pull/; echo "exit=$?"
exit=0
scp 'ryoma@nagoyadevlab02:~/share/a.txt' pull/; echo "exit=$?"
exit=0
scp -O 'ryoma@nagoyadevlab02:~/share/a.txt' pull/; echo "exit=$?"
exit=0| リモート側のパスの書き方 | 既定のモード(SFTP) | -O(旧プロトコル) |
|---|---|---|
share/*.txt | exit=0(3つ取得) | exit=0 |
share/sales report.txt | exit=0 | exit=1(share/salesとreport.txtに分かれる) |
share/sales\ report.txt | exit=0 | exit=0 |
share/{a,b}.txt | exit=1(No such file or directory) | exit=0(a.txtとb.txtを取得) |
share/[ab].txt | exit=0 | exit=0 |
$HOME/share/a.txt | exit=1(No such file or directory) | exit=0 |
~/share/a.txt | exit=0 | exit=0 |
自分で打つときは既定のモードのまま、相手のシェルの文法を持ち込まない書き方(引用符で囲んだ素のパスと、*・?・[ ])にそろえるのがおすすめです。昔からあるスクリプトが波かっこや$変数に頼っているなら、書き直すまでのつなぎとして-Oを付けます。OpenSSHのリリースノートは、SFTP方式ではシェル向けの二重の引用が要らなくなり、付けたままだと転送が失敗しうること、旧方式に合わせた互換動作は入れない方針であることを明記しています。
-Oを付けている間は、リモート側のパスが相手のシェルで解釈されます。上の$HOMEが展開されたのと同じ仕組みで、リリースノートは、引用が足りないファイル名が相手側でシェルのコマンドとして解釈されうると説明しています。外から受け取ったファイル名をそのまま-O付きのscpに渡すスクリプトは、既定のモードに直す優先度が高い箇所です。
ポート・鍵・設定ファイル・踏み台:-P、-i、~/.ssh/config、-J
ポートは大文字の-P
sshdが22番以外で待ち受けているサーバーには、大文字の-Pでポートを渡します。
scp -P 2222 report.csv ryoma@nagoyadevlab02:inbox/port.csv; echo "exit=$?"
exit=0sshコマンドの癖で小文字にすると、こうなります。
scp -p 2222 report.csv ryoma@nagoyadevlab02:inbox/; echo "exit=$?"
scp: stat local "2222": No such file or directory
exit=255
ssh ryoma@nagoyadevlab02 "stat -c '%a %s %.19y %n' inbox/report.csv"
640 57 2026-10-13 07:43:17 inbox/report.csvscpの小文字の-pは「権限と時刻を保つ」なので、2222はコピー元のファイル名として扱われ、そんなファイルはないというエラーになりました。終了コードは255で、後ろに書いたreport.csvも送られていません。2つ目のコマンドで見たinbox/report.csvの更新時刻は今日の日付のままで、-p付きで届いていれば付くはずの元の日時(1月15日の09:30:00)になっていません。
sshコマンドは逆で、ポートは小文字の-pです。大文字の-Pはssh(1)のマニュアルでは設定を選ぶためのタグ名の指定で、エラーにならずに22番へつながりました。
ssh -p 2222 ryoma@nagoyadevlab02 'echo $SSH_CONNECTION'; ssh -P 2222 ryoma@nagoyadevlab02 'echo $SSH_CONNECTION'
172.18.0.2 57216 172.18.0.3 2222
172.18.0.2 35716 172.18.0.3 22SSH_CONNECTIONの4つの値は、接続元のアドレスとポート、接続先のアドレスとポートです。1つ目の接続は相手側が2222番、-P 2222と書いた2つ目は22番になっています。
鍵を指定する-iと、設定ファイルのHostエントリ
3つ目のマシンは、専用の鍵でしか入れないようにします。まずホスト鍵を登録し(指紋を、環境の節で3つ目のマシンが表示した値と見比べます)、既定とは別の名前で鍵を作ります。
ssh-keyscan -t ed25519 nagoyadevlab03 | tee -a ~/.ssh/known_hosts | ssh-keygen -lf -
256 SHA256:fJ7iLpJQm56C/2SiXmqwjFh1d1CZRfplks/lRboZts4 nagoyadevlab03 (ED25519)
ssh-keygen -q -t ed25519 -N "" -C "ryoma@nagoyadevlab01-for-lab03" -f ~/.ssh/id_ed25519_lab03 && cat ~/.ssh/id_ed25519_lab03.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNMnnX9tQ29KxZJArJHrNXBeGgNfJSkPnhFaz6LG1HU ryoma@nagoyadevlab01-for-lab03表示された公開鍵を、3つ目のマシンの作業ユーザーに登録します。
install -d -m 700 ~/.ssh && echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNMnnX9tQ29KxZJArJHrNXBeGgNfJSkPnhFaz6LG1HU ryoma@nagoyadevlab01-for-lab03" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keysこの状態で、いつもどおりにscpを打つと失敗します。
scp report.csv ryoma@nagoyadevlab03:; echo "exit=$?"
ryoma@nagoyadevlab03: Permission denied (publickey).
scp: Connection closed
exit=255相手に登録したのはid_ed25519_lab03ですが、指定なしのscpが相手に見せたのは既定の名前の~/.ssh/id_ed25519だけでした(つまずきポイントの節で-vの出力を確かめます)。-iで鍵を指定すると通ります。
scp -i ~/.ssh/id_ed25519_lab03 report.csv ryoma@nagoyadevlab03:; echo "exit=$?"
exit=0毎回-iを打つ代わりに、~/.ssh/configに書いておけます。scpはsshと同じ設定ファイルを読みます。
cat >> ~/.ssh/config <<'EOF'
Host lab03
HostName nagoyadevlab03
User ryoma
IdentityFile ~/.ssh/id_ed25519_lab03
IdentitiesOnly yes
Host lab02-alt
HostName nagoyadevlab02
User ryoma
Port 2222
EOF
chmod 600 ~/.ssh/configHost lab03:scpやsshで打つ別名です。この名前で呼んだときだけ、下の設定が効きます。HostNameとUser:実際の接続先とユーザー名です。scp ファイル lab03:と書くだけでryoma@nagoyadevlab03につながります。IdentityFileとIdentitiesOnly yes:使う鍵を決めます。IdentitiesOnly yesは、ssh_config(5)の説明では、エージェントがほかの鍵を持っていても設定した鍵だけを使う指定です。Host lab02-altのPort 2222:ポートを覚えさせる例です。-Pを打ち忘れることがなくなります。
scp report.csv lab03:via-alias.csv; echo "exit=$?"
exit=0
scp report.csv lab02-alt:inbox/via-alias.csv; echo "exit=$?"
exit=0どちらも-iや-Pなしで届きました。別名でつないだときに相手へ見せている鍵も確かめておきます。
scp -v report.csv lab03: 2>&1 | grep 'Offering public key'
debug1: Offering public key: /home/ryoma/.ssh/id_ed25519_lab03 ED25519 SHA256:i9vAbrjZZF9NV8+K1Eh3394tYgW2090JK4OvIIAoAuU explicit見せているのはid_ed25519_lab03だけです。設定ファイルの書き方そのものは、ssh-keygenでSSH鍵を作る記事の「ssh configの書き方」の節で扱っています。
踏み台を経由する-J
-Jは、先に踏み台へ接続し、そこから目的のサーバーへの転送を張らせる指定です(マニュアルではProxyJumpの短縮形)。経路が変わることを、サーバーから見た接続元のアドレスで確かめます。
hostname -i; ssh lab03 'hostname -i'
172.18.0.2
172.18.0.4
ssh ryoma@nagoyadevlab02 'echo ${SSH_CONNECTION%% *}'; ssh -J lab03 ryoma@nagoyadevlab02 'echo ${SSH_CONNECTION%% *}'
172.18.0.2
172.18.0.4クライアントのアドレスは172.18.0.2、3つ目のマシンは172.18.0.4です。直接つないだときにサーバーが見た接続元は前者、-J lab03を付けたときは後者になりました。scpも同じ指定で通ります。
scp -J lab03 report.csv ryoma@nagoyadevlab02:inbox/via-jump.csv; echo "exit=$?"
exit=0この検証のネットワークでは、クライアントからサーバーへ直接も届きます。「踏み台を通らないと届かない」構成は再現しておらず、確かめたのは接続が3つ目のマシンを経由することまでです。
-Jに別名ではなく実際のホスト名を書くと、失敗しました。
scp -J ryoma@nagoyadevlab03 report.csv ryoma@nagoyadevlab02:inbox/via-jump.csv; echo "exit=$?"
ryoma@nagoyadevlab03: Permission denied (publickey).
Connection closed by UNKNOWN port 65535
scp: Connection closed
exit=255ryoma@nagoyadevlab03は設定ファイルのHost lab03に一致しないので専用の鍵が使われず、踏み台の認証で止まっています。踏み台にもHostエントリの設定が要るときは、-Jには別名のほうを書きます。
サーバー同士のコピー:既定は手元経由、-Rはサーバーから直接
コピー元とコピー先の両方をリモートにすると、サーバーから別のサーバーへコピーできます。手元のクライアントは、サーバーには既定の鍵で、3つ目のマシンにはlab03の設定で入れる状態です。
scp ryoma@nagoyadevlab02:inbox/report.csv lab03:from-lab02.csv; echo "exit=$?"
exit=0
scp -v ryoma@nagoyadevlab02:inbox/report.csv lab03:from-lab02.csv 2>&1 | grep -E 'Connecting to|Sending (subsystem|command)'
debug1: Connecting to nagoyadevlab03 [172.18.0.4] port 22.
debug1: Sending subsystem: sftp
debug1: Connecting to nagoyadevlab02 [172.18.0.3] port 22.
debug1: Sending subsystem: sftp成功しました。-vの出力では、クライアントが3つ目のマシンとサーバーの両方に自分で接続し、どちらにもSFTPを要求しています。マニュアル(scp(1))は、この手元を経由する転送(-3)が既定だと説明していて、OpenSSHのリリースノートには8.7で既定になったとあります。-3を明示しても結果は同じで、-pを付ければ権限と時刻も運ばれました。
scp -3 -p ryoma@nagoyadevlab02:inbox/deploy-p.sh lab03:; echo "exit=$?"; ssh lab03 "stat -c '%a %s %.19y %n' from-lab02.csv deploy-p.sh"
exit=0
640 57 2026-10-13 07:43:40 from-lab02.csv
777 22 2026-02-01 12:00:00 deploy-p.sh-Rは、コピー元のサーバーに入り、そこでscpを実行させる方式です。
scp -R ryoma@nagoyadevlab02:inbox/report.csv ryoma@nagoyadevlab03:direct.csv; echo "exit=$?"
Host key verification failed.
scp: Connection closed
exit=1
scp -v -R ryoma@nagoyadevlab02:inbox/report.csv ryoma@nagoyadevlab03:direct.csv 2>&1 | grep -E 'Connecting to|Sending (subsystem|command)|verification failed'
debug1: Connecting to nagoyadevlab02 [172.18.0.3] port 22.
debug1: Sending command: scp -v inbox/report.csv ryoma@nagoyadevlab03:direct.csv
debug1: Connecting to nagoyadevlab03 [172.18.0.4] port 22.
Host key verification failed.こちらは失敗しました。-vの出力を見ると、クライアントはサーバーにscp -v inbox/report.csv ryoma@nagoyadevlab03:direct.csvというコマンドを送り、サーバー自身が3つ目のマシンへ接続しようとして、ホスト鍵の検証で止まっています。サーバー(例では nagoyadevlab02)の作業ユーザーには、3つ目のマシンのホスト鍵も、そこへ入るための鍵も置いていないからです。
既定の手元経由なら、2つのサーバーはお互いを知らなくてよく、鍵は手元に置いたままで済みます。その代わり、データは手元の回線を通ります。-Rはデータが手元を通らない方式ですが、マニュアルにあるとおり、コピー元のサーバーから相手へパスワードなしで認証できることが条件です。サーバーに別のサーバーへ入る鍵を置くことになるので、まず既定のまま使い、手元の回線が細くて困ったときに初めて検討する順番をおすすめします。-Rが成功する構成は、この検証では作っていません。
帯域制限の-lと圧縮の-Cを測る
圧縮が効くファイルと効かないファイルを用意します。numbers.txtは連番のテキスト、random.binは/dev/urandomから取った乱数です。
seq 1 3000000 > numbers.txt && head -c 16M /dev/urandom > random.bin && ls -l numbers.txt random.bin
-rw-r--r-- 1 ryoma ryoma 22888896 Oct 13 07:43 numbers.txt
-rw-r--r-- 1 ryoma ryoma 16777216 Oct 13 07:43 random.bin-vの出力の終わりに出るTransferred:の行には、回線に流れたバイト数と時間が出ます。-Cなしと-Cありで、2つのファイルを順に送りました。
scp -v numbers.txt ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 22909500, received 16948 bytes, in 0.4 seconds
scp -v -C numbers.txt ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 6142056, received 9408 bytes, in 1.2 seconds
debug1: compress outgoing: raw data 22898068, compressed 6129758, factor 0.27
scp -v random.bin ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 16793500, received 13172 bytes, in 0.3 seconds
scp -v -C random.bin ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 16801312, received 8008 bytes, in 0.8 seconds
debug1: compress outgoing: raw data 16784046, compressed 16789604, factor 1.00numbers.txtは、-Cでsentの値が22909500から6142056に減りました(compress outgoingの行のfactor 0.27)。ところが時間はin 0.4 secondsからin 1.2 secondsに増えています。random.binはfactor 1.00で減らず、時間だけがin 0.3 secondsからin 0.8 secondsに増えました。同じホスト上のコンテナ同士の転送なので、縮めた分の得より圧縮にかかった時間のほうが大きく出た、と読めます。それぞれ1度ずつの計測で、ここから言えるのは「この環境のこの2つのファイルではこうだった」までです。
-lはKbit/s単位の上限です。numbers.txtを-l 16000で、-Cなしと-Cありで送ります。
scp -v -l 16000 numbers.txt ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 22910308, received 13448 bytes, in 11.2 seconds
scp -v -C -l 16000 numbers.txt ryoma@nagoyadevlab02:inbox/ 2>&1 | grep -E '^Transferred|compress outgoing'
Transferred: sent 6140504, received 9388 bytes, in 11.2 seconds
debug1: compress outgoing: raw data 22898104, compressed 6128152, factor 0.27-Cなしがin 11.2 seconds、-Cありがin 11.2 secondsでした。-Cを足した2つ目は、回線に流れた量が6140504まで減っているのに、時間はほとんど変わっていません。この2つの結果は、-lの上限が圧縮前のデータ量に対してかかっていると考えると説明がつきます。
使いどころはこう考えています。-lは、業務時間中に細い回線で大きなファイルを送るとき、ほかの通信を押しのけないために付けます。-Cは、回線が細く、ログやCSVのように縮むデータを送るときの候補です。ただし今回の計測では-lと併用しても時間は縮まず、乱数のように縮まないデータでは時間が増えただけでした。細い回線での効果は、自分の回線でTransferred:の行を見比べて判断してください。
中断した転送:scpは先頭から送り直す
サーバーに、OLD-CONTENTという行を繰り返した50,331,648バイトのファイルをhead -c 48Mで作ります。あとで新旧を見分けられるように、中身を決まった文字列にしました。これを-l 40000で帯域を絞って取りにいき、timeoutに3秒で打ち切らせます。
ssh ryoma@nagoyadevlab02 'yes OLD-CONTENT | head -c 48M > backup.bin && ls -l backup.bin && sha256sum backup.bin'
-rw-rw-r-- 1 ryoma ryoma 50331648 Oct 13 07:44 backup.bin
5e65da3a0c71c1fc897e1050b9e2d20f25812873c4175c19aa0f0b538f1e6e1c backup.bin
timeout 3 scp -l 40000 ryoma@nagoyadevlab02:backup.bin part-a.bin; echo "exit=$?"; ls -l part-a.bin
exit=124
-rw-r--r-- 1 ryoma ryoma 14622720 Oct 13 07:44 part-a.binexit=124はtimeoutが打ち切ったときの値で、手元には書きかけのpart-a.binが残りました。同じ宛先へ、今度は打ち切らずにscpをやり直し、受信したバイト数を-vで見ます。
scp -v ryoma@nagoyadevlab02:backup.bin part-a.bin 2>&1 | grep '^Transferred'; ls -l part-a.bin; sha256sum part-a.bin
Transferred: sent 17884, received 50369816 bytes, in 0.3 seconds
-rw-r--r-- 1 ryoma ryoma 50331648 Oct 13 07:44 part-a.bin
5e65da3a0c71c1fc897e1050b9e2d20f25812873c4175c19aa0f0b538f1e6e1c part-a.binreceived 50369816 bytesと、ファイル全体より少し多い量を受け取っています。書きかけの分を飛ばさず、先頭から全部を受け取り直したということです。scp(1)のマニュアルのオプション一覧にも、続きから再開する指定はありません。
続きだけを取りたいときは、sftpの-a(対話モードのregetと同じ動作)を使います。もう一度同じ条件で中断させてから試しました。
timeout 3 scp -l 40000 ryoma@nagoyadevlab02:backup.bin part-b.bin; echo "exit=$?"; ls -l part-b.bin
exit=124
-rw-r--r-- 1 ryoma ryoma 14622720 Oct 13 07:44 part-b.bin
sftp -v -a ryoma@nagoyadevlab02:backup.bin part-b.bin 2>&1 | grep -E '^Resuming|^Transferred'; ls -l part-b.bin; sha256sum part-b.bin
Transferred: sent 14592, received 35737488 bytes, in 0.2 seconds
Resuming /home/ryoma/backup.bin to part-b.bin
-rw-r--r-- 1 ryoma ryoma 50331648 Oct 13 07:44 part-b.bin
5e65da3a0c71c1fc897e1050b9e2d20f25812873c4175c19aa0f0b538f1e6e1c part-b.bin受信は35737488 bytesで済み、ハッシュはサーバー側と一致しました。rsyncなら--append-verifyです。
timeout 3 scp -l 40000 ryoma@nagoyadevlab02:backup.bin part-c.bin; echo "exit=$?"; ls -l part-c.bin
exit=124
-rw-r--r-- 1 ryoma ryoma 14622720 Oct 13 07:44 part-c.bin
rsync --append-verify --stats ryoma@nagoyadevlab02:backup.bin part-c.bin | grep -E 'Literal data|Total bytes received'; ls -l part-c.bin; sha256sum part-c.bin
Literal data: 35,708,928 bytes
Total bytes received: 35,717,725
-rw-r--r-- 1 ryoma ryoma 50331648 Oct 13 07:44 part-c.bin
5e65da3a0c71c1fc897e1050b9e2d20f25812873c4175c19aa0f0b538f1e6e1c part-c.binLiteral data: 35,708,928 bytesは、ファイル全体から書きかけの14,622,720バイトを引いた値と同じで、こちらもハッシュが一致しています。reget・reputの手順はSFTPの記事にまとめています。
ただし「続きから」で正しいファイルになるのは、書きかけの部分が相手のファイルの先頭と同じときです。すでに完全なファイルがある宛先へ上書きしていて中断した場合は、そうなりません。サーバーのbackup.binを、同じサイズで中身がNEW-CONTENTのファイルに作り直し、完成しているpart-a.bin(中身はOLD-CONTENT)への上書きを3秒で打ち切ります。
ssh ryoma@nagoyadevlab02 'yes NEW-CONTENT | head -c 48M > backup.bin && sha256sum backup.bin'
717e684c262bc70600ddaf8b510412c933871904a4d84f76a5d626fdde3aeba6 backup.bin
timeout 3 scp -l 40000 ryoma@nagoyadevlab02:backup.bin part-a.bin; echo "exit=$?"; ls -l part-a.bin; head -n 1 part-a.bin; tail -n 1 part-a.bin; sha256sum part-a.bin
exit=124
-rw-r--r-- 1 ryoma ryoma 50331648 Oct 13 07:44 part-a.bin
NEW-CONTENT
OLD-CONTENT
fc7fde1e62e3364060718a63f1eadefc9309086ffcc6199dbb0cf6f03c70a8cf part-a.binサイズは50,331,648バイトのまま、先頭の行はNEW-CONTENT、最後の行はOLD-CONTENTです。頭の側が新しい内容に書き換わり、末尾の側は古い内容のままというファイルで、ハッシュ(fc7fde1e…)は古い内容の5e65da3a…とも新しい内容の717e684c…とも一致しません。上書きの節では送る方向で、宛先を先に空にせず最後に長さをそろえる動きを見ました。受け取る方向でも、途中で止めるとこの状態で残ります。ここに再開のコマンドを使ってみます。
sftp -a ryoma@nagoyadevlab02:backup.bin part-a.bin; echo "exit=$?"; sha256sum part-a.bin
Connected to nagoyadevlab02.
File "part-a.bin" was not modified
Resuming /home/ryoma/backup.bin to part-a.bin
exit=0
fc7fde1e62e3364060718a63f1eadefc9309086ffcc6199dbb0cf6f03c70a8cf part-a.bin
rsync --append-verify ryoma@nagoyadevlab02:backup.bin part-a.bin; echo "exit=$?"; sha256sum part-a.bin
exit=0
fc7fde1e62e3364060718a63f1eadefc9309086ffcc6199dbb0cf6f03c70a8cf part-a.binsftp -aはwas not modifiedと表示してexit=0、rsync --append-verifyもexit=0で終わり、ハッシュは壊れたままでした。サイズが相手と同じファイルを、どちらも書き換えていません。sftp(1)のマニュアルも-aについて、書きかけの中身が転送するものと違っていれば結果は壊れたファイルになりうると断っています。直すには、全体を送り直します。
scp ryoma@nagoyadevlab02:backup.bin part-a.bin; echo "exit=$?"; sha256sum part-a.bin
exit=0
717e684c262bc70600ddaf8b510412c933871904a4d84f76a5d626fdde3aeba6 part-a.bin送る方向でも同じことが起きます。手元のpart-b.bin(中身はOLD-CONTENT)を、NEW-CONTENTになっているサーバーのbackup.binへ上書きし、3秒で打ち切ります。
timeout 3 scp -l 40000 part-b.bin ryoma@nagoyadevlab02:backup.bin; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'ls -l backup.bin; head -n 1 backup.bin; tail -n 1 backup.bin; sha256sum backup.bin'
exit=124
-rw-rw-r-- 1 ryoma ryoma 50331648 Oct 13 07:44 backup.bin
OLD-CONTENT
NEW-CONTENT
9fb8191be83105920d208199e37613d03b911435d7451cbec323ee65c9f4c86a backup.binサーバー側のファイルは50,331,648バイトのまま、今度は先頭の行がOLD-CONTENT、最後の行がNEW-CONTENTになりました。打ち切らずに全体を送り直すと、送ったファイルと同じハッシュ(5e65da3a…)になります。
scp part-b.bin ryoma@nagoyadevlab02:backup.bin; echo "exit=$?"; ssh ryoma@nagoyadevlab02 'sha256sum backup.bin'
exit=0
5e65da3a0c71c1fc897e1050b9e2d20f25812873c4175c19aa0f0b538f1e6e1c backup.bin既存のファイルへ上書きしている途中でscpが止まったら、sftp -aやrsync --append-verifyで続けず、全体を送り直してください。上の例のとおり、サイズが同じで終了コードも0のまま、頭だけが新しく残りが古いファイルが残ります。サイズでは見分けられないので、sha256sumで比べます。
スクリプトから使う:BatchMode・終了コード・別名で送ってから差し替える
cronやCIから動かすscpは、質問に答える人がいない前提で組みます。ここまでの結果を踏まえて、1ファイルを送るスクリプトを書きました。
cat > push.sh <<'EOF'
#!/bin/sh
dest=$1
file=$2
opts='-o BatchMode=yes -o ConnectTimeout=10'
scp $opts -p "$file" "$dest:inbox/$file.part" || { echo "NG: scp exit=$?"; exit 1; }
want=$(sha256sum < "$file")
got=$(ssh $opts "$dest" "sha256sum < 'inbox/$file.part'") || { echo "NG: ssh exit=$?"; exit 1; }
[ "$want" = "$got" ] || { echo "NG: checksum mismatch"; exit 1; }
ssh $opts "$dest" "mv 'inbox/$file.part' 'inbox/$file'" || { echo "NG: mv exit=$?"; exit 1; }
echo "OK: $file -> $dest:inbox/$file"
EOF-o BatchMode=yes:パスワードやホスト鍵の確認を尋ねさせず、その場で失敗させます(ssh_config(5)の説明)。質問待ちのまま止まり続けるジョブを作らないためです。-o ConnectTimeout=10:相手が応答しないときに、接続を10秒であきらめます。-pと.part:時刻と権限を保ったまま、いったん別名で送ります。本番の名前へ直接上書きしていて途中で止まると、中断の節で見たとおり、頭だけが新しいファイルがその名前で残るからです。sha256sumの比較:手元と相手のハッシュを比べます。exit=0とサイズの一致だけでは中身の正しさを保証できないことは、中断の節で見たとおりです。mv:検証が済んでから、同じディレクトリの中の改名で差し替えます。- 各行の
|| { …; exit 1; }:失敗したらその場で止め、どの段階のどの終了コードかを1つの行に残します。
使わなかった指定も書いておきます。-o StrictHostKeyChecking=noは、ssh_config(5)によれば、変わってしまったホスト鍵でも接続を進める設定です。接続先のすり替えに気づけなくなるので入れていません。-qは次に示す理由で外しました。パスワードをスクリプトに書いて渡す方法も採りません。鍵認証にして、鍵の置き場所をHostエントリで決めます。
成功したときの出力です。inboxに.partの付いたファイルは残っておらず、report.csvは-pで送られたので更新時刻が元の1月15日になっています。
sh push.sh ryoma@nagoyadevlab02 report.csv; echo "exit=$?"
OK: report.csv -> ryoma@nagoyadevlab02:inbox/report.csv
exit=0
ssh ryoma@nagoyadevlab02 "ls inbox | grep report; stat -c '%a %s %.19y %n' inbox/report.csv"
report.csv
640 57 2026-01-15 09:30:00 inbox/report.csv失敗するとどう見えるかも確かめます。1つ目はHostエントリの別名ではなく実際のホスト名を渡した場合(専用の鍵が使われない)、2つ目は存在しないファイルを渡した場合です。
sh push.sh ryoma@nagoyadevlab03 report.csv; echo "exit=$?"
ryoma@nagoyadevlab03: Permission denied (publickey).
scp: Connection closed
NG: scp exit=255
exit=1
sh push.sh ryoma@nagoyadevlab02 nofile.csv; echo "exit=$?"
scp: stat local "nofile.csv": No such file or directory
NG: scp exit=255
exit=1どちらもscp自身のメッセージに続けてNG: scp exit=255が出ています。ここで-qを付けていると、理由の行が消えます。
scp -q -o BatchMode=yes report.csv ryoma@nagoyadevlab03:; echo "exit=$?"
scp: Connection closed
exit=255Permission denied (publickey).の行がなくなり、scp: Connection closedしか残りません。マニュアルの説明どおり、-qは進捗に加えてsshの警告と診断メッセージも止めます。端末のないジョブでは進捗はもともと出ないので、-qを付けても失うものしかありません。
終了コードについて、scp(1)のマニュアルが定めているのは「成功で0、エラーで0より大きい値」までです。リモート側のコピー元がない場合を足したうえで、この記事の記録に出た値を表にします。
scp ryoma@nagoyadevlab02:inbox/nofile.csv got/; echo "exit=$?"
scp: inbox/nofile.csv: No such file or directory
exit=1| 状況 | メッセージ | 終了コード |
|---|---|---|
| 成功 | (なし) | 0 |
| リモート側のコピー元がない | scp: inbox/nofile.csv: No such file or directory | 1 |
| 宛先のディレクトリがない | scp: dest open "archive/": Failure | 1 |
-rなしでディレクトリを指定 | scp: local "site" is not a regular file | 1 |
-Rでサーバーから相手への接続に失敗 | Host key verification failed. | 1 |
| 手元のコピー元がない | scp: stat local "nofile.csv": No such file or directory | 255 |
| 認証に失敗 | Permission denied (publickey). | 255 |
timeoutによる打ち切り | (なし) | 124(timeoutの値) |
255は接続や認証の失敗だけでなく、手元のファイルが見つからないときにも出ました。1か255かで処理を分けず、0以外はすべて失敗として扱い、理由はメッセージで読むのが安全です。
動作確認:ハッシュ・権限・終了コードで確かめる
自分の環境で同じことを確かめる手順です。まず版を見ます。マニュアルの履歴の節によれば、9.0以降なら既定の転送はSFTPです。
ssh -V
OpenSSH_10.2p1 Ubuntu-2ubuntu3.7, OpenSSL 3.5.5 27 Jan 2026次に、1ファイルを-p付きで送り、終了コード、両側のハッシュ、相手側の権限・サイズ・更新時刻を見ます。
scp -p report.csv ryoma@nagoyadevlab02:inbox/check.csv; echo "exit=$?"
exit=0
sha256sum report.csv; ssh ryoma@nagoyadevlab02 'sha256sum inbox/check.csv'
a2fff68a1b76281fd3e52e569e225b8d1ae2dc91a4a38b13344619f3212924f4 report.csv
a2fff68a1b76281fd3e52e569e225b8d1ae2dc91a4a38b13344619f3212924f4 inbox/check.csv
ssh ryoma@nagoyadevlab02 "stat -c '%a %s %.19y %n' inbox/check.csv"
640 57 2026-01-15 09:30:00 inbox/check.csvexit=0であること、ハッシュの2つの値が同じであること、最後の行が手元のreport.csvの値(640・57・2026-01-15 09:30:00)と同じであることが確認できれば、指定どおりに届いています。プロトコルは、基本の節のscp -v … | grep -E 'Sending (subsystem|command)'でSending subsystem: sftpが出るかどうかで見分けます。
つまずきポイント
not a regular file
scp ryoma@nagoyadevlab02:inbox got/; echo "exit=$?"
scp: download inbox/: not a regular file
exit=1ディレクトリを-rなしで指定したときのメッセージです。サーバーから取ってくる方向では上のscp: download inbox/: not a regular file、手元から送る方向ではディレクトリの節で見たscp: local "site" is not a regular fileになります。-rを付ければ通りますが、その前にディレクトリの中のシンボリックリンクと、宛先がすでにあるかどうかを確かめてください。
Permission denied (publickey)
scp -v report.csv ryoma@nagoyadevlab03: 2>&1 | grep -E 'Offering public key|Permission denied'
debug1: Offering public key: /home/ryoma/.ssh/id_ed25519 ED25519 SHA256:+JCt5OBWKKS+RdeABzXa9bj9kTU2kHIGyr8QNIc8mGI
ryoma@nagoyadevlab03: Permission denied (publickey).-vの出力から、相手に見せた鍵の行を抜き出しています。見せているのは/home/ryoma/.ssh/id_ed25519だけで、相手に登録してあるid_ed25519_lab03は出てきません。今回再現したのはこの「鍵の指定漏れ」で、直し方は-iで鍵を指定するか、Hostエントリの別名(例ではlab03)でつなぐことです。見せた鍵が合っているのに断られる場合は、相手のauthorized_keysにその公開鍵の行があるかを確認します。
よく似た文言でも、次のメッセージは別物です。
ssh ryoma@nagoyadevlab02 'chmod 444 inbox/data.txt'; scp report.csv ryoma@nagoyadevlab02:inbox/data.txt; echo "exit=$?"
scp: dest open "inbox/data.txt": Permission denied
scp: failed to upload file report.csv to inbox/data.txt
exit=1行頭がユーザー名@ホスト名:ではなくscp: dest openで、終了コードは1です。認証は通っていて、宛先のファイル(ここでは直前に444にしたdata.txt)に書き込む権限がありません。直すのは鍵ではなく、相手側のファイルやディレクトリの権限です。
ワイルドカードに一致するファイルがない
scp 'ryoma@nagoyadevlab02:inbox/*.log' got/; echo "exit=$?"
scp: inbox/*.log: No such file or directory
exit=1
ssh ryoma@nagoyadevlab02 'ls -d inbox/*.csv'
inbox/check.csv
inbox/port.csv
inbox/renamed.csv
inbox/report.csv
inbox/via-alias.csv
inbox/via-jump.csv一致するファイルが1つもないと、パターンの文字列がそのままファイル名のように表示されます。sshで相手側の一覧を出し、実在する名前に合わせてパターンを直します。
Host key verification failed.
scp -o BatchMode=yes report.csv ryoma@scp-command-usage-options-lab02:inbox/; echo "exit=$?"
Host key verification failed.
scp: Connection closed
exit=255同じサーバーでも、known_hostsに登録したときの名前(例ではnagoyadevlab02)と違う名前でつなぐと、未登録の相手として扱われます。ここではコンテナ名でつなぎました。実機なら、手で入るときは短い名前、cronに書いたのはIPアドレスやFQDN、という食い違いで起きます。端末のない実行では確認の質問ができないので、その場で失敗します。その名前でホスト鍵を取り、指紋が環境の節でサーバーが表示した値(SHA256:oS/m…)と同じであることを確かめてから登録します。
ssh-keyscan -t ed25519 scp-command-usage-options-lab02 | tee -a ~/.ssh/known_hosts | ssh-keygen -lf -
256 SHA256:oS/mi9aR4c0TKGJw+UU1qSWh5xNSdELHPPfUHM+xSk8 scp-command-usage-options-lab02 (ED25519)
scp -o BatchMode=yes report.csv ryoma@scp-command-usage-options-lab02:inbox/; echo "exit=$?"
exit=0-Rでのサーバー同士のコピーで出た同じメッセージは、手元ではなくコピー元のサーバーが相手を知らないことが原因でした。どのマシンのknown_hostsの話なのかを、-vのConnecting toの行で切り分けてください。
検証環境を片付ける
検証が終わったら、Dockerのホストでコンテナとネットワークを消します。
docker rm -fv scp-command-usage-options-lab01 scp-command-usage-options-lab02 scp-command-usage-options-lab03
scp-command-usage-options-lab01
scp-command-usage-options-lab02
scp-command-usage-options-lab03
docker network rm scp-command-usage-options-net
scp-command-usage-options-netscp・sftp・rsyncの使い分けと「scpは非推奨か」
OpenSSHのリリースノートは、8.0で、ファイル転送にはscpのプロトコルではなくsftpやrsyncを使うよう勧め、9.0でscpコマンドの転送を既定でSFTPに切り替えました。勧められなくなったのは旧SCPプロトコルのほうで、scpコマンドは検証した10.2p1にも入っています。参照した8.0・8.7・8.8・9.0のリリースノートに、コマンドそのものを廃止するという記述はありませんでした(執筆時点)。
そのうえで、3つの道具を今回の結果で比べます。
| 判断基準 | scp(既定のモード) | sftp | rsync |
|---|---|---|---|
| 1度きりのコピーの手数 | コマンド1つ | sftp ホスト:パスの1つ、またはバッチファイル | コマンド1つ |
| 中断したあと続きから送る | できなかった(全体を受信し直した) | -aで続きだけ受信した | --append-verifyで続きだけ受信した |
| ツリーの中のシンボリックリンク | 実体のコピーになった | この記事では未確認 | -aでリンクのまま届いた |
| 同じ宛先へ繰り返し送る | -rはやり直すと宛先の中に入れ子ができた | この記事では未確認 | 差分だけを送る(別記事) |
| 相手側に要るもの | sshdのSFTPサブシステム | sshdのSFTPサブシステム | 相手側にもrsyncコマンド(別記事) |
私たちの基準は単純です。人が手で数個のファイルを運ぶならscp、サイズが大きく途中で切れそうならsftpの-aかrsync、同じディレクトリを毎日そろえるならrsync、取引先に受け渡し口を渡すならSFTP専用のアカウントです。最後の構成では旧プロトコルのscp -Oとrsyncが通らないことを、SFTPの記事で確かめています。
Windowsのscp(この環境では未実行)
Microsoftのドキュメント(執筆時点)によると、OpenSSHはWindows 10 build 1809とWindows Server 2019以降でオンデマンド機能として提供されていて、同梱コマンドの一覧にscpがあります。入っているかどうかはWindowsの種類と版で違うので、PowerShellで次の2つを打って確かめることになります。
ssh -V
scp report.csv ryoma@nagoyadevlab02:inbox/この2つのコマンドは、この環境では実行していません。検証に使ったのはLinuxのコンテナだけです。表示された版が9.0以降なら既定がSFTP方式、という読み方の根拠はLinuxと同じOpenSSHのリリースノートですが、Windows版でのパスの書き方や権限の扱いは確認できていません。LinuxからWindowsへ送る向きでは、Windows側でSSHサーバー(sshd)を動かす必要があります。こちらも試していません。
よくある質問
scpコマンドは何の略ですか?
secure copyを縮めた名前で、OpenSSHのマニュアルの見出しは「OpenSSH secure file copy」です。同じマニュアルの履歴の節には、BSDのrcpというプログラムをもとにしていると書かれています。書式はcpと同じ「コピー元 コピー先」の並びで、ホスト名とコロンを付けたほうがリモート側のパスになります。
scpコマンドは非推奨ですか?
OpenSSHが勧めなくなったのは旧来のSCPプロトコルです。リリースノートは8.0で代わりにsftpやrsyncを挙げ、9.0でscpコマンドの転送を既定でSFTPに切り替えました。コマンド自体は検証した10.2p1にも入っていて、-Oを付けない限り旧プロトコルは使われませんでした。
scpの設定はどこに書きますか?
scpはsshと同じ~/.ssh/configを読みます。HostエントリにHostName・User・Port・IdentityFileを書けば、scp ファイル 別名:パスだけで届くことを本文で確かめました。サーバー側はsshdが動いていて、SFTPのサブシステムが有効であれば、既定のscpを受けられます。
Windowsのコマンドでもscpは使えますか?
Microsoftのドキュメントによると、OpenSSHはWindows 10 build 1809とWindows Server 2019以降でオンデマンド機能として提供され、scpはその同梱コマンドの1つです(執筆時点)。入っているかはssh -Vで確かめます。Windows上での動作は、この記事では検証していません。
まとめ
いまのscpは、-Oを付けない限り、名前は同じでも中身はSFTPです。コピー先の権限は-pなしなら相手のumask、-pありなら元のまま、-rはリンクを実体にして運び、リモート側のパスは相手のシェルを通りません。中断すると先頭からやり直しになり、上書きの途中で止まると、頭だけが新しいファイルが元の長さのまま残ります。大きなファイルや繰り返しの同期はsftp -aとrsyncに任せ、スクリプトでは別名で送ってハッシュを比べてから差し替えます。最初の一歩は、手元でscp -v 適当なファイル ユーザー名@ホスト名: 2>&1 | grep -E 'Sending (subsystem|command)'を打ち、Sending subsystem: sftpが出るかを見ることです。続けて、手持ちのスクリプトにリモート側の波かっこ・$変数・二重の引用が残っていないかを探してください。