scp Permission denied:遠端複製權限排錯
同一行 Permission denied 可能來自遠端目錄不可寫、本機檔案讀不到,或目標在 root 底下而 scp 無法使用 sudo。逐一分辨這三種情況,並處理磁碟滿、唯讀掛載與 SELinux 這些看起來一樣的假象。
scp Permission denied 的三種原因
scp 複製失敗時印出的 Permission denied,底下其實藏著三種完全不同的狀況:遠端的目的地目錄不讓你寫入、本機這一端讀不到來源檔案,或者目的地是 root 擁有的路徑,而 scp 沒有任何方式提權。第三種是最多人卡住的地方。一次複製的過程裡沒有 sudo 可以用。先分辨是哪一種,再決定做法,不要先亂改權限。
一次 scp 是一個 SFTP(SSH file transfer protocol,SSH 檔案傳輸協定)工作階段,不是一個遠端 shell。你在遠端的身分就是你登入的那個帳號,從頭到尾沒有換人的機會。
本文的例子都需要第二台主機,所以請把 deploy@198.51.100.20 換成你自己的帳號與伺服器位址再執行。
先確認這不是登入階段被拒絕
有兩種 Permission denied 長得很像,但發生的時間點差很多。認證失敗的那一種會把方法寫在括號裡,而且不會提到任何路徑:
deploy@198.51.100.20: Permission denied (publickey).這一行代表 scp 連都還沒連上,檔案傳輸根本沒有開始,所以檢查目錄權限是浪費時間。它屬於 登入階段就被拒絕的 Permission denied (publickey) 那一類問題,先用 ssh deploy@198.51.100.20 單獨登入一次確認。本文處理的是另一種:ssh 可以登入,scp 卻在搬檔案的時候失敗,訊息裡帶著一個具體的路徑。
遠端目錄不讓你寫的時候,訊息長什麼樣
scp ./site.conf deploy@198.51.100.20:/etc/nginx/conf.d/site.confscp: dest open "/etc/nginx/conf.d/site.conf": Permission denied
scp: failed to upload file ./site.conf to /etc/nginx/conf.d/site.conf關鍵字是 dest open,意思是遠端那一側要把目的檔案開起來寫入時失敗了。判斷方式很直接:用同一個帳號登入,問它自己能不能寫。
ssh deploy@198.51.100.20 'id; ls -ld /etc/nginx/conf.d; test -w /etc/nginx/conf.d && echo dir-writable || echo dir-NOT-writable'test -w 是由 kernel 實際判斷的結果,比你自己盯著權限欄位推算可靠。這裡要分開看兩件事:能不能在一個目錄裡建立新檔案,由那個目錄的權限決定;能不能覆寫一個已經存在的檔案,由那個檔案自己的權限決定。所以目錄可寫、檔案屬於 root 的情況下,第一次複製會成功,覆寫時卻失敗。權限欄位每一格代表什麼,交給 讀懂 drwxr-xr-x 這一串權限欄位,本文不重複。
還要記得 scp 建立出來的新檔案,擁有者一定是你登入的那個帳號,因為檔案是那個帳號的工作階段建立的。-p 只會盡量保留時間戳與模式,它不會、也不可能幫你換擁有者。
本機讀不到來源檔案的時候,訊息長什麼樣
同一個指令,錯誤也可能發生在你自己的電腦上:
scp: open local "./backup.tar.gz": Permission deniedopen local 這三個字就是答案,遠端完全沒有被牽連。最常見的來源是你用 sudo 產生的備份檔,或是路徑中某一層目錄少了進入的權限。整條路徑一次看完:
ls -l ./backup.tar.gz
namei -l "$PWD/backup.tar.gz"namei -l 會把路徑拆成每一層並逐層列出權限,所以少了執行權限的那一層目錄會直接現形。走不進目錄,就讀不到裡面的檔案,即使檔案本身看起來是可讀的。要改擁有者的話,做法在 chown 怎麼改擁有者與群組。
為什麼 scp 裡面沒有 sudo
這是多數人真正缺的那一塊知識。scp 在遠端啟動的是 sshd 的 sftp 子系統,不是一個可以打字的 shell。沒有 TTY(terminal,終端機),沒有指令列,也沒有任何地方可以輸入 sudo 密碼。所以 sudo 在一次複製裡沒有位置可以放,這不是選項寫錯,而是協定裡沒有這個東西。
scp ./site.conf deploy@198.51.100.20:"sudo tee /etc/nginx/conf.d/site.conf"上面這種寫法不會提權,它只是把一個很奇怪的字串當成檔名來用。同樣地,直接改用 root 登入也不是答案:多數 sshd 設定是 PermitRootLogin prohibit-password 或 no,而且把部署用的帳號換成 root,稽核紀錄就分不出是誰動的手。金鑰該怎麼發、怎麼收回,見 SSH 金鑰管理的基本做法。
結論很簡單:提權要發生在一個正常的遠端指令裡,不要試圖塞進複製本身。下面四種做法都是照這個原則走的。
做法一:先送到家目錄,再用一般連線搬過去
這是最安全、也最不需要事先設定的做法。你的家目錄你自己可寫,所以複製一定過得去;提權留給第二條指令。
scp ./site.conf deploy@198.51.100.20:~/site.conf
ssh -t deploy@198.51.100.20 'sudo install -o root -g root -m 644 ~/site.conf /etc/nginx/conf.d/site.conf'ssh -t 會配一個 TTY,所以 sudo 有地方讓你輸入密碼。install 比 mv 好用,因為它在同一步就把擁有者、群組與模式明確設定好,不必依賴搬移過程幫你猜。上面的 644 是你自己決定的值,請按那個服務真正需要的權限填,不要照抄。
檢查結果,並且確認服務讀得到新檔案:
ssh deploy@198.51.100.20 'ls -l /etc/nginx/conf.d/site.conf; sudo nginx -t'ls -l 應該顯示 root 擁有,nginx -t 應該回 syntax is ok 與 test is successful。任何一邊不對,先看是檔案沒到位,還是內容本身有問題。
做法二:rsync 搭配遠端的特權 helper
rsync 和 scp 不同,它允許你指定遠端要執行哪一個程式:
rsync -av --rsync-path="sudo rsync" ./site.conf deploy@198.51.100.20:/etc/nginx/conf.d/遠端跑起來的是 sudo rsync,所以寫入是以 root 身分進行的。但這條路有一個前提,而且失敗訊息很明確:
sudo: no tty present and no askpass program specified
rsync: connection unexpectedly closed (0 bytes received so far) [sender]第一行是 sudo 說的:這個工作階段沒有 TTY,它沒辦法問你密碼。所以這個做法要求那個帳號對 rsync 有免密碼的 sudo 權限:
ssh -t deploy@198.51.100.20 'sudo visudo -f /etc/sudoers.d/rsync-deploy'deploy ALL=(root) NOPASSWD: /usr/bin/rsync請用 visudo 而不是一般編輯器,因為它在存檔前會檢查語法,寫壞 sudoers 檔案是會把自己鎖在門外的。同時要清楚這條規則的代價:能以 root 身分執行任意 rsync,實際上就等於 root,因為 rsync 可以覆寫系統上任何檔案。願意接受的話,這是 CI 部署最順的一條路;不願意接受的話,回到做法一。
做法三:把目標目錄的擁有者設對,之後就不用再對抗它
如果你是反覆往同一個路徑丟東西,例如部署目錄,那就不要每次都提權。設定一次就好:
ssh -t deploy@198.51.100.20 'sudo install -d -o deploy -g deploy /srv/app/releases'
scp ./release.tar.gz deploy@198.51.100.20:/srv/app/releases/install -d 會把目錄連同擁有者一起建好。之後所有複製都以普通身分完成,不需要 sudo、不需要 NOPASSWD,也不會有人半夜對著 Permission denied 發呆。這條做法適用於那些本來就該由應用程式帳號擁有的目錄,不要拿它去改 /etc 底下的東西。
做法四:整個目錄用 tar 串過去
要搬的是一整棵目錄樹,而且在意擁有者與權限有沒有保留的時候,scp -r 不是好工具。把 tar 接在 ssh 後面,解壓縮那一端就是一個正常的遠端指令,sudo 在那裡是合法的:
tar czf - -C ./dist . | ssh deploy@198.51.100.20 'sudo tar xzf - -C /srv/app'注意這裡沒有 -t,因為標準輸入已經被管線佔用了,所以這條指令同樣需要遠端 sudo 免密碼,否則你會看到和做法二一樣的 no tty present。完整的語法、權限保留與常見陷阱,見 用 tar 把整個目錄透過 ssh 串到另一台機器。
目標目錄不存在,看起來也像權限問題
路徑打錯是最常被誤判成權限的狀況。現代的 scp 其實會說得比較清楚:
scp: upload "/srv/app/releases/site.conf": path canonicalization failed或是這一行:
scp: target directory "/srv/app/releases" does not exist兩句話說的都是路徑本身不存在,不是你沒有權限。舊版的客戶端,或你加了 -O 走舊協定時,訊息會變成 No such file or directory。確認方式:
ssh deploy@198.51.100.20 'ls -ld /srv/app/releases'還有一個很容易中的細節。目的地結尾有沒有斜線,決定了 scp 把它當目錄還是當檔名。目的地是一個不存在的目錄而你寫了 /,它就失敗;沒寫 /,它會很開心地建立一個叫 releases 的檔案,然後你以為成功了。
磁碟滿了或掛載成唯讀,回報的是 Failure
這一點值得記下來,因為它是最好用的分辨依據。遠端的 sftp-server 會把 errno 轉成協定狀態碼,而只有 EACCES 與 EPERM 會變成 Permission denied。磁碟滿(ENOSPC)與唯讀掛載(EROFS)都落在預設分支,所以回來的是一個很籠統的字:
scp: write remote "/srv/backup/dump.sql": Failure看到 Failure 而不是 Permission denied,就知道不要再去查權限了。改查空間與掛載狀態:
ssh deploy@198.51.100.20 'df -h /srv/backup; df -i /srv/backup; findmnt -T /srv/backup'df -h 是容量,df -i 是 inode 數量,兩者任一耗盡都會給你 ENOSPC,而 inode 用光時容量欄位還很漂亮,所以只看 df -h 會找不到原因。findmnt -T 會印出那個路徑實際的掛載選項,欄位裡出現 ro 就是唯讀。空間明明應該夠卻顯示滿了的情況,看 df 與 du 對不上的磁碟滿問題。
Rocky 與 AlmaLinux 上的 SELinux
目標是 Enterprise Linux 9 系列(Rocky Linux、AlmaLinux)的時候,多了一個變數。SELinux 的拒絕同樣走 EACCES,所以它確實會變成 Permission denied,而 ls -l 看起來一切正常。先確認它有沒有在強制模式:
ssh deploy@198.51.100.20 'getenforce; ls -Zd /var/www/html'最常見的情況其實不是 scp 本身失敗,而是複製成功之後服務讀不到。原因是這樣:直接在 /var/www/html 裡面建立的新檔案會繼承那個目錄的 context(安全標籤),所以沒事;但你若先把檔案放在家目錄,再用 sudo mv 搬進去,mv 會連同家目錄的標籤一起搬過去,於是 web server 被 SELinux 擋下,而你的 ls -l 顯示權限與擁有者都正確。這正是做法一要小心的地方。
ssh -t deploy@198.51.100.20 'sudo restorecon -Rv /var/www/html; sudo ausearch -m AVC -ts recent'restorecon 會依照系統政策把標籤改回該有的值,-v 會列出它改了哪些檔案。ausearch -m AVC 讀的是稽核紀錄裡的拒絕事件,有 denied 字樣的那幾行就是證據。用 install 而不是 mv(做法一的寫法)可以從一開始就避開這個問題,因為 install 是在目的地建立新檔案。
現代 scp 走的是 SFTP 協定
網路上很多關於 scp 錯誤訊息的答案已經過期了,因為底層協定換過。OpenSSH 9.0 的發行說明寫得很明確:scp(1) 從舊的 scp/rcp 協定改為預設使用 SFTP 協定,並且提供 -O 讓你退回舊行為。Ubuntu 24.04 的 openssh 套件版本是 9.6p1(截至 2026 年 9 月,noble 的更新仍在 9.6p1 這一系列),所以它的 scp 預設就是走 SFTP。先看清楚你手上是哪一版:
ssh -V
ssh deploy@198.51.100.20 'ssh -V'這件事對排錯有兩個實際影響。第一,錯誤訊息的用字變了,dest open、open local、path canonicalization failed 這些字串都是新協定的產物,拿舊文章的關鍵字去搜會搜不到。第二,遠端如果把 Subsystem sftp 從 sshd_config 裡註解掉,或那台機器根本沒有 sftp-server,新版 scp 會這樣失敗:
subsystem request failed on channel 0這一行和權限無關。這時 scp -O 反而會成功,因為舊協定是把 scp 當成一個普通遠端指令來執行的。舊協定同時還保留了很麻煩的引號處理規則,所以請把 -O 當成應付舊伺服器的臨時手段,不要當成日常習慣。
一份可以照著走的排查順序
- 讀錯誤訊息裡的第一個字串。
open local代表問題在你的電腦上,dest open或write remote代表問題在遠端。 - 看括號。訊息結尾是
(publickey)而且沒有路徑,那是登入失敗,不是檔案權限。 - 看狀態字。
Permission denied才是權限,Failure幾乎都是空間或唯讀掛載。 - 用同一個帳號登入,跑
id、ls -ld與test -w,讓 kernel 直接回答它能不能寫。 - 確認目的地路徑真的存在,並注意結尾斜線有沒有寫。
- 目標是 Rocky 或 AlmaLinux 就加看
getenforce、ls -Z與ausearch -m AVC。 - 確定是需要 root 才寫得進去的路徑,就選上面四種做法之一,不要試著在複製指令裡塞
sudo。
FAQ
scp 可以用 sudo 嗎?
不行。scp 在遠端啟動的是 sshd 的 sftp 子系統,那裡沒有 shell、沒有 TTY,也沒有任何地方輸入 sudo 密碼,所以提權這件事在協定層面就不存在。把 sudo 寫進目的地字串只會被當成檔名的一部分。要寫入 root 擁有的路徑,改成先複製到自己的家目錄,再用 ssh -t 開一個正常連線用 sudo install 搬過去;或者改用 rsync --rsync-path="sudo rsync",代價是那個帳號需要對 rsync 有免密碼 sudo 權限。
怎麼分辨錯誤發生在本機還是遠端?
看訊息裡的前綴。scp: open local "...": Permission denied 是你自己電腦上的檔案讀不到,遠端毫無關係,通常是那個檔案由 sudo 產生,或路徑中某一層目錄少了進入的權限,用 namei -l 逐層檢查最快。scp: dest open "...": Permission denied 配上 failed to upload file 則是遠端拒絕建立或覆寫檔案,這時才需要去看遠端目錄與檔案的擁有者。
權限看起來都對,為什麼還是 Permission denied?
先確認你查的是對的東西:建立新檔案看的是目錄權限,覆寫既有檔案看的是那個檔案的權限,兩者常常不一致。都對的話,若目標是 Rocky Linux 或 AlmaLinux,用 getenforce 確認 SELinux 是否在 enforcing,再用 sudo ausearch -m AVC -ts recent 找有 denied 的紀錄,因為 SELinux 的拒絕也是 EACCES,訊息長得一模一樣而 ls -l 完全正常。另一種情況是檔案已經傳過去了,只是服務讀不到,那通常是用 mv 從家目錄搬進來時把舊標籤帶了過去,跑 sudo restorecon -R 重新標記即可。
為什麼有時候錯誤是 Failure 而不是 Permission denied?
因為遠端的 sftp-server 會把 errno 轉成協定狀態碼,而只有 EACCES 與 EPERM 會對應到 Permission denied。磁碟空間用盡的 ENOSPC 與唯讀掛載的 EROFS 都落進預設分支,所以你看到的是籠統的 Failure,例如 scp: write remote "/srv/backup/dump.sql": Failure。遇到這個字就別查權限了,改跑 df -h、df -i 與 findmnt -T,inode 用光時容量欄位還是很正常,只看 df -h 會漏掉。
我的 scp 訊息和網路上的文章都不一樣,是版本問題嗎?
是。OpenSSH 9.0 的發行說明指出 scp(1) 已從舊的 scp/rcp 協定改為預設使用 SFTP 協定,錯誤訊息的用字因此整批換過。Ubuntu 24.04 帶的是 openssh 9.6p1(截至 2026 年 9 月),預設走 SFTP,所以 dest open 這類字串不會出現在較舊的教學裡。先用 ssh -V 確認兩端版本。如果遠端沒有啟用 Subsystem sftp,新版 scp 會失敗在 subsystem request failed on channel 0,這時 -O 可以退回舊協定暫時解決,但它同時帶回舊協定麻煩的引號規則,不建議長期使用。