readonlyまたはdeclare -rは現在のshellでvariableを再代入・unsetできないようにします。設定値の意図しない上書きを早く検出する助けにはなりますが、secret保護やOS access controlではありません。sourceされたscriptがglobal readonlyを作るとcallerへ解除不能な副作用を残します。
結論は「script全体の定数はreadonly、function内はlocal -rを使い、値のvalidation後に固定します。secretをreadonlyへ入れて安全だと思わず、scopeとexportを最小にします」です。
readonlyの役割とsecurity境界を分ける
Bash builtin readonlyはnameをreadonlyにし、後のassignmentやunsetを拒否します。declare -rも同様のattributeを付けます。function内のlocal -rはscopeをfunctionへ閉じられます。readonly -pやdeclare -pで状態を表示できますが、値にsecretがあれば出力へ露出します。
- 定数にする値がvalidation済みか
- global・function local・environmentのどのscopeか
- callerがoverrideするconfigurationか
- exportしてchild processへ渡す必要があるか
- readonly値にsecretが含まれないか
configuration precedenceをdefault、environment、argument、config fileの順で決め、最終値を確定してからreadonlyにします。先にreadonlyへすると正当なoverrideもerrorになります。command substitutionで値を得る場合はassignment statusを確認してから固定します。
readonlyへする前に、値がdefault、環境変数、option、設定fileのどの順で決まり、validation済みかを確認します。global定数、function内の引数、array、exportする環境値を分け、readonly -pまたはdeclare -pで現在属性を読みます。sourceされるlibraryがgenericなglobal名をreadonlyにすると呼び出し元へ解除不能の副作用を残すため、namespaceとscopeを確認します。
validation後に値を固定する
global constantはUPPER_SNAKE_CASE等の規約でreadonlyへし、function inputはlocal -rで受けます。arrayもreadonly -aまたはdeclare -raを使えますが、要素変更が拒否されることをtestします。debug時にreadonly -pを共有する場合は値をmaskします。
validated defaultを固定
api_root=${API_ROOT:-https://api.example.test}
case $api_root in https://*) ;; *) printf '%s\n' 'invalid API root' >&2; exit 2;; esac
readonly API_ROOT_FINAL=$api_root
environment overrideをvalidateした後に別nameで固定します。
declare -rで定数
declare -r RETRY_LIMIT=3
printf 'retry_limit=%s\n' "$RETRY_LIMIT"
再代入を想定しないintegerを明示します。
function local readonly
show_file() {
local -r file=$1
printf '%s\n' "$file"
}
caller shell全体を汚さずfunction scopeで再代入を防ぎます。
readonly array
readonly -a SUPPORTED_ENVS=(dev stage prod)
printf '%s\n' "${SUPPORTED_ENVS[@]}"
array要素も固定されるため、実行中に追加するlistには使いません。
readonly属性を表示
readonly -p
全readonly値が出るためsecretを含むsessionでは共有・recordingを避けます。
設定優先順位を解決し、型・範囲・URL scheme等を検証した後にreadonly NAME=valueまたはdeclare -r NAME=valueで固定します。function内ではlocal -r arg=$1を使い、global汚染を避けます。arrayはdeclare -raまたはreadonly -a、associativeならdeclare -rAをBash版に合わせます。command substitutionのstatusを確認してから固定し、失敗結果や空値をreadonlyにしません。
再代入errorとscopeの仕組み
readonly attributeは同じshell processでのassignment/unsetを防ぎ、shell終了まで解除できません。child processはparent variableを変更できません。exportされたreadonly valueをchildが自身のenvironmentで変更してもparentへ戻りません。readonlyはfile permission、encryption、credential vaultの代わりではありません。
- readonly nameとdeclare -rはvariableへreadonly attributeを付ける
- readonly variableは同じshellで再代入・unsetできない
- local -rはfunction scopeへreadonly variableを作る
- readonly -pはreadonly nameとvalueを表示する
- readonlyはsecretの閲覧やprocess memory露出を防ぐsecurity boundaryではない
readonlyは同じshell processで以後のassignmentとunsetを拒否する属性で、暗号化やaccess controlではありません。shell終了まで解除できず、child processへexportされた値はchild側で別の環境として扱われます。local -rはfunction scopeに限られますが、Bashのdynamic scopingにより呼出しfunctionから見える場合があるため、変数名衝突をtestします。
secretとsourceされたscriptの副作用を防ぐ
password/tokenをreadonlyにしてstdout、xtrace、process environmentから守れると考えません。secret managerやfile descriptor等の適切な仕組みを使います。library scriptをsourceする場合はglobal readonly name collisionを避け、namespaceとdocumented interfaceを設けます。
- validation前の値をreadonlyにする
- readonlyを暗号化・access controlと考える
- source fileがgeneric global nameをreadonlyにする
- readonly -pをpublic logへ出す
- runtimeで変えるstateをconstantにしてerrorを隠す
passwordやtokenをreadonlyにしてもprocess memory、environment、trace、core dump、readonly -p出力から守れません。secret managerやfile descriptor等の適切な受け渡しを使います。sourceされるfileでPATH、IFS、HOME等をreadonlyにするとcallerを壊すため避けます。実行途中で変わるstateやcounterを誤って固定し、error回避のためsubshellを乱用しないよう設計します。
固定値とenvironment overrideをtestする
default、valid override、invalid override、function call、再代入attempt、array element変更、child shellをtestします。期待する場所だけerrorになり、callerのunrelated variableが影響を受けないことを確認します。
- configuration precedenceとvalidation後に値を固定した
- globalよりlocal -rを優先してscopeを限定した
- secretをreadonlyの保護へ依存していない
- 再代入・override・child processのtestを行った
default、正しいoverride、不正override、空値、function再入、再代入、unset、array要素変更、child shellをtestします。期待した箇所だけassignmentが拒否され、無関係なcaller変数へ影響しないか確認します。source libraryなら新しいBashで読み込み前後のdeclare -pを比較し、global readonly名がdocumented interface以外に増えていないかを見ます。
readonlyを使う値と使わない設定を判断する
不変であることがprogram contractならreadonlyを使い、運用で変更する設定はvalidation済みconfigurationとして扱います。source libraryではglobal readonlyを最小化し、実行scriptではsubshell/process境界で副作用を隔離します。
プログラム中に変えてはいけないvalidated値やfunction引数の意図を示すためreadonlyを使います。運用で変更するconfigurationは起動時に解決・validationしてから別のfinal名へ固定します。security境界が必要ならreadonlyへ頼らず、OS permission、secrets管理、process分離を使います。libraryではglobal readonlyを最小化し、実行script側で定数を確定します。

コメント