こんにちは、おうどんです🍜
うどん屋さんの朝。 店長(おうどん)が、新しいレシピを書いてきました。
店長「今日はこのレシピで作って!」
門番さん「お待ちください。そのレシピ、どなたが書きました?」
店長「ぼく。店長」
門番さん「このお店では、レシピの持ち込みはお断りしております」
店長「……ここ、ぼくの店なんだけど」
(場面は説明用の架空のものです)
これ、Windows の PowerShell(Windows に入っているコマンドの実行環境)で、本当によく起きます。 自分で書いた .ps1(PowerShell のスクリプトファイル)を実行しただけなのに、こう言われるんです。
このシステムではスクリプトの実行が無効になっているため、ファイル C:\...\hello.ps1 を読み込むことができません。
自分のパソコンで、自分のファイルを動かしただけなのに?
はい。 しかも門番さんは、レシピの中身を1文字も読んでいません。
今回は、この門番さん(実行ポリシー)の正体と、安全な通り方を、初心者向けの直し方から、ダウンロードしたファイルの見えないシール、タスクスケジューラでの書き方、優先順位の内部の話まで掘っていきます。
結論:門番を呼んでいるのは「実行ポリシー」
このエラーの犯人は、壊れたファイルでも権限不足でもありません。PowerShell の実行ポリシーが、スクリプトの実行を止めています。
実行ポリシー(Execution Policy)は、PowerShell がスクリプトや設定ファイルを読み込んでよい条件を決める仕組みです。 about_Execution_Policies のドキュメントでは、Windows のクライアント(Windows 10・11 などのパソコン向けの OS)の既定が Restricted(スクリプトは実行できない)、Windows Server の既定が RemoteSigned と書かれています。
まずは、この3つだけ覚えて帰ってください。
Get-ExecutionPolicy -Listで、今どのルールが効いているかを見る- 自分のスクリプトを動かしたいだけなら、自分のユーザーだけ
RemoteSignedにする - ダウンロードしたスクリプトは、中身を確かめてから
Unblock-Fileする
門番さん、クビにすればよくない?
クビにする方法(Unrestricted や Bypass を常時にする)もあります。 でも、そうすると知らない人のレシピも、全部そのまま厨房に入ってきます。 門番さんには、ルールを変えて残ってもらうのがおすすめです。
じゃあ、門番さんに店長の顔を覚えてもらえば?
それが、あとで出てくる「署名」(デジタル署名、書いた人を証明する仕組み)です。 ただ、顔パスの準備は、ちょっと大がかりです。今日は、もっと手軽な方法から行きます。
エラーを再現:1行なら通るのに、ファイルにすると止まる
不思議なのはここです。Restricted でも、コマンドを1行ずつ打つのは通ります。止まるのは、ファイルから読み込むときだけです。
ドキュメントでも、Restricted は「個々のコマンドは許可するが、スクリプトは許可しない」と書かれています。 試してみます。hello.ps1 の中身は1行だけです。
hello.ps1:
Write-Output "こんにちは、うどんです"
まず、ファイルとして実行します(-ExecutionPolicy Restricted は、このセッションだけ Restricted にする指定。あとで詳しく出てきます)。
powershell -NoProfile -ExecutionPolicy Restricted -File hello.ps1
このシステムではスクリプトの実行が無効になっているため、ファイル C:\...\ep\hello.ps1 を読み込むことができません。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
+ CategoryInfo : セキュリティ エラー: (: ) []、ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnauthorizedAccess
(今回の検証環境の Windows 10 Pro・Windows PowerShell 5.1.19041.7725 で実行した結果です。コンソールの折り返しをつなげ、ファイルのパスを短くしています。終了コードは 1 でした)
次に、同じ1行をコマンドとして打ちます。
powershell -NoProfile -ExecutionPolicy Restricted -Command 'Write-Output "こんにちは、うどんです"'
こんにちは、うどんです
(今回の検証環境で実行した結果です。終了コードは 0 でした)
中身はまったく同じなのに、ファイルだと止まり、1行だと通りました。
門番さん「レシピの持ち込みは、お断りです」
店長「じゃあ口で言うね。麺を茹でて、つゆをかけて……」
門番さん「どうぞ、お入りください」
店長「……さっきのレシピと、一字一句同じなんだけど」
……門番さんが見ているのは、中身ではなく「紙で持ってきたかどうか」です。
エラー文のキーワードは UnauthorizedAccess と about_Execution_Policies。 この2つが出ていたら、ファイルの権限(アクセス権)ではなく、実行ポリシーの話です。
UnauthorizedAccess って書いてあるから、フォルダの権限を全部フルコントロールにしてきた。
……門番さんに止められたのに、金庫の鍵を全部開けてきた人です。 権限を戻して、次の章に進みましょう。
Get-ExecutionPolicy -List で、どの指示書が効いているか見る
直す前に、現状を見ます。門番さんへの指示書は5枚あって、上にあるものほど強いんです。
Get-ExecutionPolicy Get-ExecutionPolicy -List
AllSigned
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser Undefined
LocalMachine AllSigned
(今回の検証環境で、powershell -NoProfile -Command から実行した結果です)
Get-ExecutionPolicy だけだと、今効いているポリシー(実効ポリシー)が1つ出ます。 -List を付けると、スコープ(設定が効く範囲)ごとの設定が、強い順に並びます。
| スコープ | 意味 | 誰が決める |
|---|---|---|
| MachinePolicy | パソコン全体へのグループポリシー | 会社の管理者など |
| UserPolicy | そのユーザーへのグループポリシー | 会社の管理者など |
| Process | 今の PowerShell のセッションだけ | 起動した人・プログラム |
| CurrentUser | 今のユーザーだけ | 自分 |
| LocalMachine | パソコンの全ユーザー | 管理者 |
グループポリシー(会社などで、パソコンの設定を一括で管理する Windows の仕組み)は、上の2行です。 Undefined は「この指示書は白紙」という意味です。全部白紙なら、Windows のクライアントでは Restricted になる、とドキュメントにあります。
今回の検証環境は、LocalMachine が AllSigned(すべてのスクリプトに署名が必要)になっていました。 このとき、自分で書いた署名なしのスクリプトを実行すると、エラー文が変わります。
ファイル C:\...\ep\local.ps1 を読み込めません。ファイル C:\...\ep\local.ps1 はデジタル署名されていません。このスクリプトは現在のシステムでは実行できません。
(今回の検証環境で -ExecutionPolicy AllSigned を付けて実行した結果の冒頭部分だけです。コンソールの折り返しをつなげ、パスを短くしています)
エラー文が「実行が無効」なら Restricted、「デジタル署名されていません」なら AllSigned か RemoteSigned。文面で、だいたいの見当がつきます。
門番さん「指示書が5枚あります。上から読みます」
店長「4枚、白紙だけど」
門番さん「はい。なので一番下の『署名がないレシピは全部お断り』に従います」
店長「……白紙の4枚、なんのためにあるの」
……白紙は「おまかせ」のしるしです。誰かが書いた瞬間に、そっちが勝ちます。
5枚全部に同じことを書いておけば、確実では?
確実です。 そして1年後、ルールを変えたくなったときに、5枚全部を探して書き直すことになります。
直し方:自分のユーザーだけ RemoteSigned にする
自分で書いたスクリプトを動かしたいだけなら、CurrentUser のスコープを RemoteSigned にするのが、いちばん影響の小さい直し方です。
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Get-ExecutionPolicy -List
(今回の検証環境の設定を変えないため、このコマンドは実行していません。書き方は Set-ExecutionPolicy のドキュメントの例に基づきます)
RemoteSigned は、ドキュメントでは次のルールです。
- 自分のパソコンで書いたスクリプトは、署名なしで実行できる
- インターネット(メールやチャットのアプリを含む)からダウンロードしたスクリプトは、信頼できる発行元の署名が必要
- ダウンロードしたものでも、
Unblock-Fileなどでブロックを解除すれば、署名なしで実行できる
CurrentUser を選ぶ理由です。
Set-ExecutionPolicyの既定のスコープは LocalMachine(パソコンの全ユーザー)。LocalMachine を変えるには「管理者として実行」が必要、とドキュメントにある- CurrentUser なら、影響は自分のユーザーだけ
- CurrentUser は LocalMachine より強いので、LocalMachine が Restricted や AllSigned のままでも、自分には RemoteSigned が効く
変更は、PowerShell を再起動しなくてもすぐ効きます。 戻したいときは、Undefined(白紙)にします。
Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope CurrentUser
(これも未実行です。ドキュメントの例に基づきます)
店長「門番さん、ぼくのレシピだけは通して」
門番さん「承知しました。店長が店内で書いたレシピは通します。外から持ち込まれたレシピは、これまでどおり確認します」
店長「話が早い」
……最初からこう頼めばよかった。
面倒だから、LocalMachine を Unrestricted にしちゃった。
それは「門番さん、今日から全部顔パスで」と全員分の入口で宣言したのと同じです。 家族や同僚と共有しているパソコンなら、全員に効きます。まずは CurrentUser に戻しましょう。
設定したのに、まだエラーが出る。
その場合は、自分より上の指示書(グループポリシーか Process)が効いている可能性があります。 もう一度 Get-ExecutionPolicy -List を見てください。上級者向けの章で、もう少し詳しく扱います。
🔰 ここまで読めば今日から困らない
- 「このシステムではスクリプトの実行が無効」は、実行ポリシーのしわざ。ファイルの権限の問題ではない
Get-ExecutionPolicy -Listで、どのスコープが何になっているかを見る- 自分のスクリプトを動かしたいだけなら、
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - 戻すときは
-ExecutionPolicy Undefined -Scope CurrentUser
4行。門番さんへのお願いの手紙より短い。
手紙を書く前に、まず指示書を5枚とも読む。 これだけで「なんで止まるの」の半分は解決します。
門番さん、意外と話せば分かる人だった。
そうなんです。 門番さんは意地悪ではなく、「うっかり知らないレシピで料理しないように」立っている人です。この話は、上級者向けの章で回収します。
ここから先は中級者向け。読み飛ばしてもOKです。
中級:RemoteSigned なのに止まる→ダウンロードの印(Zone.Identifier)と Unblock-File
RemoteSigned にしたのに、ネットから落としてきたスクリプトだけ止まる。 これはファイルに、目に見えない「インターネットから来ました」のシールが貼られているからです。
ドキュメントによると、Windows では Microsoft Edge などのプログラムが、ダウンロードしたファイルに代替データストリーム(ADS、NTFS というファイルの仕組みで、1つのファイルに付けられる裏の小部屋のようなデータ)を追加して、インターネットから来たファイルとして印を付けます。 この小部屋の名前が Zone.Identifier です。Unblock-File のドキュメントには、値が 3 だとインターネットからダウンロードされたことを表す、と書かれています。
今回は、ダウンロードしたファイルを用意するかわりに、Python で dl2.ps1 に ZoneId=3 の Zone.Identifier を書き込んで再現しました(検証用の細工)。
まず、シールが貼られたファイルを探します。
Get-Item .\*.ps1 -Stream Zone.Identifier -ErrorAction SilentlyContinue |
ForEach-Object { "{0} {1} {2}" -f (Split-Path $_.FileName -Leaf), $_.Stream, $_.Length }
Get-Content .\dl2.ps1 -Stream Zone.Identifier
dl2.ps1 Zone.Identifier 26
[ZoneTransfer]
ZoneId=3
(今回の検証環境で、上の2つのコマンドを書いた .ps1 を -ExecutionPolicy Bypass -File で実行した結果です)
Get-Item の -Stream で、小部屋のあるファイルだけが見つかります(PowerShell 3.0 から)。 Get-Content -Stream で、小部屋の中身も読めます。
このシール付きのファイルを、RemoteSigned で実行すると止まります。シールのない local.ps1 は通ります。
powershell -NoProfile -ExecutionPolicy RemoteSigned -File local.ps1 powershell -NoProfile -ExecutionPolicy RemoteSigned -File dl.ps1
local.ps1 を実行しました
ファイル C:\...\ep\dl.ps1 を読み込めません。ファイル C:\...\ep\dl.ps1 はデジタル署名されていません。このスクリプトは現在のシステムでは実行できません。スクリプトの実行および実行ポリシーの設定の詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
+ CategoryInfo : セキュリティ エラー: (: ) []、ParentContainsErrorRecordException
+ FullyQualifiedErrorId : UnauthorizedAccess
(今回の検証環境で、dl.ps1 に ZoneId=3 の Zone.Identifier を付けて実行した結果です。上が local.ps1 の標準出力、下が dl.ps1 の標準エラー。dl.ps1 の終了コードは 1 でした。コンソールの折り返しをつなげ、パスを短くしています)
中身を確かめて安全だと判断したら、Unblock-File でシールをはがします。
Unblock-File -Path .\dl.ps1
$z = Get-Item .\dl.ps1 -Stream Zone.Identifier -ErrorAction SilentlyContinue
if ($z) { "Zone.Identifier: まだある" } else { "Zone.Identifier: なくなった" }
Zone.Identifier: なくなった
(今回の検証環境で、上の3行を書いた .ps1 を -ExecutionPolicy Bypass -File で実行した結果です)
powershell -NoProfile -ExecutionPolicy RemoteSigned -File dl.ps1
dl.ps1 を実行しました
(今回の検証環境で実行した結果です)
ドキュメントによると、Unblock-File は内部で Zone.Identifier の小部屋を消しています。エクスプローラーで、ファイルのプロパティ画面にあるブロック解除のボタンを押すのと同じ操作です。 実行ポリシーそのものは変わりません。
門番さん「このレシピ、外から来たシールが貼ってありますね。お断りです」
店長「シール、はがしました」
門番さん「どうぞ」
店長「……中身、見なくていいの?」
門番さん「中身を見るのは、はがした人の仕事です」
……門番さんの言うとおりで、ドキュメントも、Unblock-File の前にファイルと入手元を確認するように書いています。 シールをはがすのは、「中身を読んだ。責任は自分が持つ」というハンコです。
ひとつ、ハマりどころがあります。 ドキュメントには、curl.exe・Invoke-WebRequest・Invoke-RestMethod などでダウンロードしたファイルには、この印が付かないことがある、と書かれています。
ブラウザで落としたら止まって、curl で落としたら動いた。curl のほうが安全なんだ!
……逆です。シールを貼り忘れただけです。 中身の確認は、どちらで落としても同じだけ必要です。
中級:タスクスケジューラ・バッチから呼ぶなら、起動時に -ExecutionPolicy を渡す
タスクスケジューラ(決まった時刻にプログラムを動かす Windows の機能)やバッチファイルから .ps1 を動かすとき、パソコンの設定を変えずに済ませる方法があります。powershell.exe の起動時に -ExecutionPolicy を渡す、です。
about_PowerShell_exe のドキュメントによると、-ExecutionPolicy は今のセッションの実行ポリシーを決めて、$Env:PSExecutionPolicyPreference という環境変数に保存します。レジストリ(Windows の設定の保管庫)の設定は変えません。
powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ExecutionPolicy -List | Format-Table -AutoSize; 'PSExecutionPolicyPreference=' + `$env:PSExecutionPolicyPreference"
Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Bypass
CurrentUser Undefined
LocalMachine AllSigned
PSExecutionPolicyPreference=Bypass
(今回の検証環境で実行した結果です。実際の実行では Python の subprocess から引数を渡したので、$ の前の `(PowerShell の中から打つときのエスケープ)は付けていません。先頭の空行は省いています)
Process の行だけが Bypass になって、LocalMachine の AllSigned はそのままです。
バッチファイルから呼ぶ形です。 %~dp0 は「このバッチファイルがあるフォルダ」の意味で、ドキュメントでも、バッチから呼ぶときは .\ ではなく %~dp0 を使うように書かれています。
run_main.bat:
@echo off powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0main.ps1" echo exit code=%ERRORLEVEL%
main.ps1:
Write-Output "main.ps1 を実行しました" exit 3
main.ps1 を実行しました
exit code=3
(今回の検証環境で、Python の subprocess から run_main.bat を実行した結果です)
-File で呼ぶと、スクリプトの exit 3 が、そのまま powershell.exe の終了コードになりました。 逆に、実行ポリシーで止められたときの終了コードは、今回の検証では 1 でした。タスクスケジューラの「前回の実行結果」が 1 なら、実行ポリシーも疑う候補に入ります。
門番さん「本日だけの張り紙ですね。『この店員は、今日は全部通してよし』」
店長「明日は?」
門番さん「張り紙は、店じまいと一緒にはがします」
……張り紙を貼ったのは誰か、が分かるのが大事です。
Bypass は「何もブロックせず、警告も確認も出さない」設定で、ドキュメントでは、PowerShell を大きなアプリケーションの一部として組み込む場合などのためのもの、と説明されています。 自分で中身を管理しているスクリプトを、決まった起動方法で動かす、という使い方に向いています。
便利だから、ショートカットも全部 -ExecutionPolicy Bypass にしよう。
それをやると、ダブルクリックしたスクリプトは、どこから来たものでも通るようになります。 張り紙は、張る場所を決めて、1枚ずつ。
セッションの中から、同じことを Set-ExecutionPolicy でもできます。
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
今回の検証環境では、このあと Get-ExecutionPolicy -List の Process が Bypass になり、新しく起動した別の PowerShell では Process が Undefined に戻っていました。 Process スコープはレジストリに保存されず、セッションが終わると消える、というドキュメントの説明どおりです。
中級:.ps1 だけじゃない。モジュール(.psm1)とプロファイルも止まる
「スクリプトなんて実行してないのに、エラーが出る」というパターンもあります。モジュール(.psm1)やプロファイルの読み込みも、実行ポリシーの対象だからです。
ドキュメントでは、Restricted は、書式・設定のファイル(.ps1xml)、モジュールのスクリプトファイル(.psm1)、PowerShell のプロファイル(起動時に自動で読み込まれる .ps1)を含む、すべてのスクリプトファイルの実行を止める、と書かれています。
mymod.psm1:
function Get-Udon { "かけうどん" }
powershell -NoProfile -ExecutionPolicy Restricted -Command "Import-Module .\mymod.psm1; Get-Udon"
Import-Module : このシステムではスクリプトの実行が無効になっているため、ファイル C:\...\ep\mymod.psm1 を読み込むことができません。詳細については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?LinkID=135170) を参照してください。
発生場所 行:1 文字:1
+ Import-Module .\mymod.psm1; Get-Udon
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : セキュリティ エラー: (: ) [Import-Module]、PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess,Microsoft.PowerShell.Commands.ImportModuleCommand
Get-Udon : 用語 'Get-Udon' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
(以下略)
(今回の検証環境で実行した結果です。コンソールの折り返しをつなげ、パスを短くし、2つ目のエラーの後半を省略しています)
1つ目が本当の原因、2つ目は「モジュールが読み込めなかったので、関数がない」という巻き添えのエラーです。
Get-Udon「呼ばれたので来ました!」
PowerShell「あなたは存在しません」
Get-Udon「……ぼく、門の外に置いてかれたんですけど」
……存在を否定された理由は、門の前で止められたからです。
ここで、下のエラーだけ検索してしまうと、「関数名の打ち間違い?」と遠回りします。 エラーが2つ以上出たら、いちばん上から読む。これは実行ポリシーに限らず効きます。
下のエラーのほうが長いから、そっちが本命だと思った。
長いエラーは、たいてい巻き添えです。 声の大きい人が犯人とは限りません。
中級:完成コード|原因を一発で切り分ける点検スクリプト
ここまでの確認を1本にまとめます。実効ポリシー・スコープごとの設定・グループポリシーの有無・Process の上書き・ファイルごとの署名とダウンロード印を一度に出します。
Check-ExecutionPolicy.ps1:
param([string]$Path = ".")
"PowerShell : {0} {1}" -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion
"実効ポリシー: {0}" -f (Get-ExecutionPolicy)
Get-ExecutionPolicy -List | ForEach-Object { " {0,-13} {1}" -f $_.Scope, $_.ExecutionPolicy }
$gpo = Get-ExecutionPolicy -List |
Where-Object { $_.Scope -in 'MachinePolicy', 'UserPolicy' -and $_.ExecutionPolicy -ne 'Undefined' }
if ($gpo) {
"注意: グループポリシーで設定されています(Set-ExecutionPolicy では変えられません)"
}
if ($env:PSExecutionPolicyPreference) {
"注意: このセッションは Process スコープで {0} になっています" -f $env:PSExecutionPolicyPreference
}
"--- スクリプトの署名とダウンロード印(Zone.Identifier)"
Get-ChildItem -Path $Path -Include *.ps1, *.psm1 -Recurse -File -ErrorAction SilentlyContinue |
ForEach-Object {
$zone = Get-Content -LiteralPath $_.FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue
$zoneId = ($zone -match '^ZoneId=') -replace '^ZoneId=', ''
[pscustomobject]@{
Name = $_.Name
ZoneId = if ($zoneId) { $zoneId } else { '-' }
Signature = (Get-AuthenticodeSignature -LiteralPath $_.FullName).Status
}
} | Format-Table -AutoSize
点検スクリプト自体が止められては困るので、起動時に Bypass を渡して動かします。
powershell -NoProfile -ExecutionPolicy Bypass -File .\Check-ExecutionPolicy.ps1
PowerShell : Desktop 5.1.19041.7725
実効ポリシー: Bypass
MachinePolicy Undefined
UserPolicy Undefined
Process Bypass
CurrentUser Undefined
LocalMachine AllSigned
注意: このセッションは Process スコープで Bypass になっています
--- スクリプトの署名とダウンロード印(Zone.Identifier)
Name ZoneId Signature
---- ------ ---------
Check-ExecutionPolicy.ps1 - NotSigned
check_zone.ps1 - NotSigned
check_zone2.ps1 - NotSigned
child.ps1 - NotSigned
dl.ps1 - NotSigned
dl2.ps1 3 NotSigned
hello.ps1 - NotSigned
local.ps1 - NotSigned
main.ps1 - NotSigned
mymod.psm1 - NotSigned
unblock.ps1 - NotSigned
(今回の検証環境で、検証用のファイルを置いたフォルダで実行した結果です。日本語を含むので、スクリプトは BOM 付きの UTF-8 で保存して実行しました)
読み方です。
- 実効ポリシーが Bypass なのは、点検のために起動時に渡したから。本来の値は
-Listの Process 以外の行で見る - グループポリシーの注意が出たら、自分では変えられない。管理者に相談する
- ZoneId が 3 のファイルは、RemoteSigned でも止まる。中身を確認してから
Unblock-File - Signature が NotSigned のファイルは、AllSigned では止まる
点検スクリプト「全部 NotSigned でした!」
店長「うん、全部ぼくが書いた」
点検スクリプト「ちなみに、ぼくも NotSigned です」
……点検係が、いちばん最初に自分を申告している。正直でよろしい。
一覧に、関係ないファイルまで出てきて長い。
-Path に、調べたいスクリプトのフォルダを渡してください。 家じゅうの引き出しを開ける前に、まず台所の引き出しから。
ここから先は上級者向け。読み飛ばしてもOKです。
上級:優先順位と保存場所、そしてグループポリシーは別格
指示書5枚の強さと、それぞれがどこに書かれているかを整理します。グループポリシーが決めていれば、それがすべてに勝ち、決めていなければ Process → CurrentUser → LocalMachine の順に強い、です。
ドキュメントに書かれている保存場所です(Windows PowerShell 5.1)。
| スコープ | 保存場所 |
|---|---|
| Process | 環境変数 $Env:PSExecutionPolicyPreference(セッションが終わると消える) |
| CurrentUser | レジストリ HKCU:\Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell の ExecutionPolicy |
| LocalMachine | レジストリ HKLM:\Software\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell の ExecutionPolicy |
グループポリシーは、「スクリプトの実行を有効にする」(Turn on Script Execution)という設定で決めます。 ドキュメントによると、この設定は PowerShell の中で決めたすべてのスコープの実行ポリシーより優先されます。Set-ExecutionPolicy は MachinePolicy・UserPolicy を変えられず、自分の設定のほうが厳しくても、グループポリシーを上書きしません。
だから、こういうことが起きます。
Set-ExecutionPolicyが成功しても、実効ポリシーが変わらないことがある(ドキュメントにも明記)- 例: LocalMachine を変えても、CurrentUser の設定があればそちらが勝つ
- 例: グループポリシーがあるパソコンでは、「設定は更新したが、グループポリシーで上書きされている」という趣旨のエラーが出る(ドキュメントの例)
店長「門番さん、店長命令です。全部通して」
門番さん「本部からの通達で、署名なしはお断りです」
店長「本部……?」
門番さん「チェーン店ですので」
……個人店だと思っていたら、フランチャイズだった。
会社のパソコンでグループポリシーが決まっているなら、それは会社のルールです。 起動時の -ExecutionPolicy も、ドキュメントによるとグループポリシーより強くはなりません。管理者に相談するのが正解です。
もう1つ、見落としやすい違いがあります。 Set-ExecutionPolicy のドキュメントには、Windows PowerShell 5.1(powershell.exe)と PowerShell 6.0 以降(pwsh.exe)は実行ポリシーの保存場所が違い、別々に管理される、と書かれています。 PowerShell 7.6 版の about_Execution_Policiesでは、CurrentUser と LocalMachine は powershell.config.json という設定ファイルに保存される、と説明されています。
powershell.exe で RemoteSigned にしたのに、pwsh では止まる。
別の店の門番さんです。 同じ制服を着ていても、指示書は店ごとに別々です。
ちなみに 7.6 版のドキュメントでは、Windows 以外(Linux・macOS)では既定が Unrestricted で変更できず、実際の動きは Bypass と同じ、とされています。Windows のセキュリティゾーン(さっきのシールの仕組み)がないからです。
上級:Process スコープは子プロセスに引き継がれる
起動時の -ExecutionPolicy は「そのセッションだけ」と書きましたが、正確にはそのセッションと、そこから起動した子のセッションです。ドキュメントにも、子プロセスが閉じるまで保持される、と書かれています。
child.ps1:
"--- 親(-ExecutionPolicy Bypass で起動)" "Process=" + (Get-ExecutionPolicy -Scope Process) "--- 子(-ExecutionPolicy を付けずに起動)" powershell -NoProfile -Command "'Process=' + (Get-ExecutionPolicy -Scope Process); 'Effective=' + (Get-ExecutionPolicy)"
powershell -NoProfile -ExecutionPolicy Bypass -File child.ps1
--- 親(-ExecutionPolicy Bypass で起動)
Process=Bypass
--- 子(-ExecutionPolicy を付けずに起動)
Process=Bypass
Effective=Bypass
(今回の検証環境で実行した結果です)
子の PowerShell には -ExecutionPolicy を付けていないのに、Process が Bypass になっています。 保存場所が環境変数なので、子プロセスが環境変数ごと受け継いだ、という動きです。
親「今日は全部通してよし、の張り紙をもらいました」
子「ぼくも、もらったことになってます」
店長「……張り紙、コピーされてる」
……張り紙は、店員が連れてきたバイトさんにも効きます。
実務では、これが両方向に効きます。
- 良い面: Bypass で起動したスクリプトの中から、別の .ps1 を
powershell -Fileで呼んでも止まらない - 注意する面: Bypass で起動したセッションから起動したツールが、さらに PowerShell を呼ぶと、それも Bypass になる
もう1つ、ドキュメントには「この変数の値を書き換えてもポリシーは変えられない」と書かれています。 ところが今回の検証環境(Windows PowerShell 5.1.19041.7725)では、Restricted で起動したセッションの中で $env:PSExecutionPolicyPreference = 'Bypass' と書き換えると、その直後の Get-ExecutionPolicy が Bypass を返し、.\hello.ps1 も実行できました。 ドキュメントと食い違う動きなので、この挙動を前提にした仕組みは作らないでください。正しい方法は、起動時の -ExecutionPolicy か Set-ExecutionPolicy -Scope Process です。
じゃあ、環境変数を書き換える裏ワザで行こう。
ドキュメントに「できない」と書いてあることが、たまたま今できているだけです。 次の更新で、その裏口に鍵がかかっても文句は言えません。
上級:実行ポリシーはセキュリティ境界ではない
最後に、門番さんの本当の役目の話です。実行ポリシーは、攻撃を止める壁(セキュリティ境界)ではなく、うっかりを防ぐための仕組みです。
ドキュメントには、はっきりこう書かれています。
- 実行ポリシーはセキュリティ境界ではなく、多層防御(いくつもの対策を重ねる考え方)の1つ
- スクリプトを実行できなくても、スクリプトの中身をコマンドラインに打ち込めば、簡単に回避できる
- 実行ポリシーは、基本的なルールを決めて、うっかりそれを破らないようにするためのもの
冒頭の章で、Restricted でも1行のコマンドは通ったのを覚えているでしょうか。 あれがまさに、ドキュメントの言う「中身を打ち込めば回避できる」です。
だから、設計の判断はこうなります。
- 悪意のあるスクリプトから守りたいなら、実行ポリシーだけに頼らない。ウイルス対策、権限の最小化、入手元の確認などと組み合わせる
- 実行ポリシーは「知らないうちに、ダウンロードしたスクリプトを実行してしまう」事故を減らすために使う。RemoteSigned と Zone.Identifier の組み合わせが、ちょうどその役目
- 署名で守りたいなら AllSigned。ただしドキュメントにもあるとおり、署名されていても悪意のあるスクリプトを実行するリスクは残る
門番さん「わたしは、壁ではありません」
店長「えっ」
門番さん「うっかり知らないレシピで料理しないように、声をかける係です。本気で忍び込む人は、裏口から来ます」
……正直すぎる門番さん。でも、だからこそ信頼できる。
もう1つ、上級者向けの小ネタです。 ドキュメントによると、PowerShell はファイルのゾーン(さっきのシール)を確かめるのに Windows のデスクトップシェル(explorer.exe)の API を使います。そのため、Windows Server Core などでは「AuthorizationManager check failed.」というエラーになることがあり、Bypass か AllSigned ならゾーンを確かめないので避けられる、とされています。 ログオン時のスクリプトが、デスクトップの準備より先に動いたときにも起きうる、と書かれています。
門番さん、シールを確かめるのに、エクスプローラーさんに聞きに行ってたの?
そうなんです。 エクスプローラーさんが出勤前だと、門番さんも判断できずに止まります。
チェックリスト
- エラー文に
about_Execution_PoliciesとUnauthorizedAccessがあるか見た Get-ExecutionPolicy -Listで、どのスコープが何になっているか見た- 自分のスクリプトを動かすだけなら、CurrentUser を RemoteSigned にした(LocalMachine や Unrestricted にしていない)
- グループポリシーで決まっているなら、自分で変えずに管理者に相談した
- ダウンロードしたスクリプトは、中身と入手元を確認してから
Unblock-Fileした - curl や
Invoke-WebRequestで落としたファイルには、シールが付かないことがあると知っている - タスクスケジューラ・バッチからは
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0xxx.ps1"の形で、自分が管理しているスクリプトだけを呼んでいる - モジュール(.psm1)やプロファイルも止まることを知っている。エラーは一番上から読む
powershell.exeとpwsh.exeの設定が別々なことを知っている- 実行ポリシーをセキュリティ対策のすべてだと思っていない
10個。門番さんへの引き継ぎ書類みたいになってきた。
引き継ぎ書類は、全部読まなくても大丈夫です。 困っているのが今日なら、上の2つと3つ目だけで、たいていは解決します。
全部チェックしたら、門番さんと仲良くなれる?
なれます。 そして、門番さんに止められた日に「あ、今日はシールのほうだな」と分かるようになります。
まとめ
冒頭では、自分の店に自分のレシピを持ち込んだら、門番さんに止められました。
- 「このシステムではスクリプトの実行が無効」は、実行ポリシーのしわざ。Windows のクライアントの既定は Restricted
- Restricted でも1行ずつのコマンドは通る。止まるのはファイル(.ps1・.psm1・プロファイルなど)
Get-ExecutionPolicy -Listで、5枚の指示書を上から読む- 自分のスクリプトを動かすだけなら、
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - ダウンロードしたファイルには Zone.Identifier のシールが付く。中身を確認してから
Unblock-File - タスク・バッチからは、起動時の
-ExecutionPolicy Bypass。Process スコープは子プロセスにも引き継がれる - グループポリシーは別格。
powershell.exeとpwsh.exeは別々 - 実行ポリシーはセキュリティ境界ではない。うっかりを防ぐ係
店長「門番さん、今日からルールを変えます。店内で書いたレシピは通して、外からのレシピはシールを見て止めてください」
門番さん「承知しました。RemoteSigned ですね」
店長「……門番さん、最初から専門用語で話せたの?」
門番さん「聞かれなかったので」
……聞けば教えてくれる門番さんでした。
実行ポリシーのエラーは、壊れたのではなく「誰が、どこから持ってきたレシピか」を聞かれているだけ。答え方さえ分かれば、門は開きます。
まずは今日、PowerShell を開いて Get-ExecutionPolicy -List を1回打ってみてください。 自分の店の門番さんが、どんな指示書を持っているかが分かったら、明日のうどんは、門の前で冷めずにすみます🍜
参考資料
- Microsoft Learn:about_Execution_Policies(PowerShell 5.1) — 実行ポリシーの定義、セキュリティ境界ではないこと、各ポリシーの意味(Restricted が .ps1xml・.psm1・プロファイルも止めること)、クライアントと Server の既定、スコープと優先順位・保存場所、
powershell.exe -ExecutionPolicyと$Env:PSExecutionPolicyPreference、子プロセスまで保持されること、変数の値を書き換えても変えられないという記述、グループポリシー、ダウンロード時の代替データストリームと curl などで印が付かないことがある点、Server Core での AuthorizationManager check failed - Microsoft Learn:about_Execution_Policies(PowerShell 7.6) — PowerShell 7 で CurrentUser・LocalMachine が powershell.config.json に保存されること、Windows 以外の既定が Unrestricted で変更できず動きは Bypass と同じこと
- Microsoft Learn:Set-ExecutionPolicy(PowerShell 5.1) — 既定のスコープが LocalMachine で管理者が必要なこと、CurrentUser・Process・Undefined の例、グループポリシーを上書きしないこと、powershell.exe と pwsh.exe が別々に管理されること
- Microsoft Learn:Unblock-File(PowerShell 5.1) — Zone.Identifier(値 3)を消す仕組み、PowerShell 3.0 で追加、実行前に中身と入手元を確認すること、エクスプローラーの「ブロックの解除」と同じ操作であること、
Get-Item -Streamでの探し方 - Microsoft Learn:about_PowerShell_exe(PowerShell 5.1) —
-ExecutionPolicyがレジストリを変えずに$Env:PSExecutionPolicyPreferenceに保存すること、-Fileとexitの終了コード、バッチからは%~dp0を使うこと
確認日:2026-10-02。

コメント