# क्या forced SSH commands में AI service account को सुरक्षित रखा जा सकता है?

AI agent को कभी ऐसा SSH credential नहीं मिलना चाहिए जिसका अर्थ हो, «account जो कुछ कर सकता है, वह सब कर लो।» यह permission boundary नहीं है। यह उसे खोजने का निमंत्रण है, अक्सर ऐसे argument के ज़रिए जिसकी आपको उम्मीद नहीं थी, उस forwarded connection के ज़रिए जिसे बंद करना आप भूल गए, या उस deployment script के ज़रिए जो caller पर ज़रूरत से ज़्यादा भरोसा करती है।

Forced SSH commands authentication के बाद शुरू होने वाली प्रक्रिया पर remote server को अंतिम नियंत्रण देते हैं। Deployment और diagnostic accounts के लिए ये अच्छे विकल्प हैं, क्योंकि ये एक अस्पष्ट क्षमता, remote shell access, की जगह आपके नियंत्रण और निरीक्षण में रहने वाला एक नामित operation देते हैं। ये credential के उपयोग के लिए human approval की जगह नहीं लेते। इन दोनों controls को अलग रखें: व्यक्ति तय करता है कि agent credential इस्तेमाल कर सकता है या नहीं, और server तय करता है कि वह credential कौन-सा सीमित action कर सकता है।

## Forced command execution को सीमित करता है, authentication को नहीं

Forced command sshd को निर्देश देता है कि client shell माँगे या कोई दूसरा command भेजे, फिर भी server द्वारा चुना गया program चलाया जाए। Client पहले भी authentication करता है। यह अंतर तब तक साफ़ लगता है जब तक service account किसी agent configuration में दिखाई नहीं देता और लोग सफल login को approved deployment समझना शुरू नहीं कर देते।

OpenSSH इस control को दो जगह support करता है। आप `authorized_keys` में किसी public key के साथ `command="/path/to/wrapper"` जोड़ सकते हैं, या `sshd_config` में किसी user या group के लिए `ForceCommand` सेट कर सकते हैं। दोनों मामलों में sshd client द्वारा माँगा गया command `SSH_ORIGINAL_COMMAND` environment variable में दर्ज करता है और उसकी जगह forced program शुरू करता है।

OpenSSH का `sshd(8)` manual पहले हिस्से पर बिल्कुल स्पष्ट है: `command` option authentication के बाद बताए गए command को चलाने के लिए मजबूर करता है। वह यह भी बताता है कि मूल command forced program के लिए उपलब्ध रहता है। यही दूसरा विवरण कई कमजोर designs की वजह बनता है। Wrapper को untrusted client से एक string मिलती है। उसे इस string को request की तरह parse करना चाहिए, shell में नहीं भेजना चाहिए।

जब एक account के पास कई सावधानी से अलग किए गए credentials हों, तब per-key form इस्तेमाल करें। Release credential deployment wrapper शुरू कर सकता है, जबकि operations credential read-only diagnostic wrapper शुरू कर सकता है। इससे `authorized_keys` में intent साफ़ दिखता है और आप account की दूसरी access बदले बिना एक credential हटा सकते हैं।

जब account को authentication के तरीके की परवाह किए बिना कभी general shell नहीं देना हो, तब `ForceCommand` इस्तेमाल करें। इसमें वह password भी शामिल है जिसे बंद करना आप भूल गए हों, future certificate authority, या वह administrator जो ज़रूरी options कॉपी किए बिना एक और public key जोड़ देता है। `Match User deploy` block review के दौरान इस नियम को नज़रअंदाज़ करना कठिन बनाता है।

इन दोनों forms का इस्तेमाल human administrator account को automation account में बदलने के लिए न करें। लोगों को repair work के लिए कभी-कभी वास्तविक shell चाहिए होती है। Automation के लिए अलग Unix account, अलग credential, अलग command wrapper और काम के अनुरूप ownership boundaries दें।

## Deployment entry point का नियंत्रण server के पास होना चाहिए

Deployment account को generic command interpreter में नहीं, बल्कि आपके नियंत्रण वाली एक script में प्रवेश करना चाहिए। Script छोटी request vocabulary स्वीकार कर सकती है, लेकिन repository path, target directory, service unit और executable का चुनाव उसे स्वयं करना चाहिए।

यह `authorized_keys` entry एक credential को wrapper तक सीमित करती है और उन connection features को अस्वीकार करती है जिनकी deployment account में कोई जगह नहीं है:

```text
restrict,command="/usr/local/libexec/release-gate" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent
```

`restrict` उपयोगी है क्योंकि OpenSSH के documentation के अनुसार यह port forwarding, agent forwarding, X11 forwarding और pseudo-terminal allocation को बंद करने का संक्षिप्त तरीका है। इसका सटीक व्यवहार आपके server द्वारा support किए जाने वाले OpenSSH options पर निर्भर करता है, इसलिए इसे अपने version पर test करें। अगर आपके environment में review या compatibility के लिए explicit options चाहिए, तो उन्हें साफ़-साफ़ लिखें:

```text
command="/usr/local/libexec/release-gate",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent
```

Wrapper को free-form deployment command नहीं लेना चाहिए। Callers को fixed verbs और सीमित value दें। उदाहरण के लिए, caller केवल immutable revision के आधार पर release माँग सकता है:

```text
ssh release@deploy.example "release 9f2a7c6d1e4b8a03"
```

एक सुरक्षित wrapper उस grammar को और कुछ नहीं स्वीकार कर सकता है:

```sh
#!/bin/sh
set -eu

request=${SSH_ORIGINAL_COMMAND-}
case "$request" in
  "release "[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]* )
    revision=${request#release }
    case "$revision" in
      *" "*|*[!0-9a-f]*)
        echo "invalid revision" >&2
        exit 64
        ;;
    esac
    exec /usr/local/libexec/run-release "$revision"
    ;;
  *)
    echo "unsupported remote request" >&2
    exit 64
    ;;
esac
```

अगर आपके revision format में निश्चित लंबाई चाहिए, तो इस example में length check भी जोड़ना होगा। Production wrapper को पूरा immutable object ID या आपके द्वारा तय format वाला release identifier स्वीकार करना चाहिए। `main` जैसे branch name को स्वीकार न करें, अगर approval और deployment के बीच caller उसे बदल सकता है। Branch एक pointer है। Immutable revision से approval record, deployment log और बनने वाला artifact एक ही चीज़ का reference दे सकते हैं।

`run-release` script को absolute paths का इस्तेमाल करना चाहिए और अपना environment खुद सेट करना चाहिए। Caller द्वारा दिए गए `PATH`, working directory, locale, `GIT_DIR`, `GIT_SSH_COMMAND` या `LD_PRELOAD` पर भरोसा न करें। शुरुआत का एक न्यूनतम रूप ऐसा हो सकता है:

```sh
#!/bin/sh
set -eu
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
unset CDPATH ENV BASH_ENV GIT_DIR GIT_WORK_TREE GIT_SSH_COMMAND
cd /srv/release-repo

revision=$1
/usr/bin/git cat-file -e "$revision^{commit}"
/usr/local/libexec/build-and-activate "$revision"
```

Account को केवल उन्हीं files का owner होना चाहिए जिन्हें उसे बदलना ज़रूरी है। अगर account को service restart करनी है, तो passwordless access से किसी general package manager या shell तक पहुँच देने के बजाय fixed arguments वाला एक संकीर्ण `sudoers` command दें। जो deployment account अपना wrapper लिख सकता है, अपना `authorized_keys` बदल सकता है या अपने code को चलाने वाली unit edit कर सकता है, वह अक्सर व्यापक control हासिल कर सकता है। केवल SSH configuration नहीं, इन paths की भी जाँच करें।

## `SSH_ORIGINAL_COMMAND` input है, command line नहीं

Forced-command में सबसे आम गलती यह line है:

```sh
sh -c "$SSH_ORIGINAL_COMMAND"
```

यह आपके बनाए control को निष्प्रभावी कर देती है। Client `release goodrev; curl ... | sh`, command substitution, redirected output या सावधानी से quote किए गए ऐसे argument का अनुरोध कर सकता है जो privileged tool तक पहुँच जाए। जो wrapper `eval`, `sh -c`, `bash -c` या unquoted expansion चलाता है, वह अलग filename के तहत remote shell access फिर से बना देता है।

Full shell parser बनाने की कोशिश न करें। उसकी ज़रूरत नहीं है। जानबूझकर छोटा protocol तय करें और उससे बाहर की हर चीज़ अस्वीकार करें। Deployment account के लिए request एक verb और एक identifier हो सकती है। Diagnostic account के लिए request `health` या `version` जैसे एक exact word तक सीमित हो सकती है।

Diagnostics के लिए dispatch wrapper parsing से पूरी तरह बच सकता है:

```sh
#!/bin/sh
set -eu

case "${SSH_ORIGINAL_COMMAND-}" in
  health)
    exec /usr/local/libexec/report-health
    ;;
  queue-depth)
    exec /usr/local/libexec/report-queue-depth
    ;;
  version)
    exec /usr/local/libexec/report-version
    ;;
  "")
    echo "a diagnostic name is required" >&2
    exit 64
    ;;
  *)
    echo "diagnostic is not allowed" >&2
    exit 64
    ;;
esac
```

इन diagnostic scripts को भी अपने arguments पर नियंत्रण रखना चाहिए। `report-health` को fixed binaries को fixed local sockets या ज्ञात service names पर चलाना चाहिए। उसे host parameter लेकर `curl "$host"` नहीं चलाना चाहिए और न ही journal filter लेकर उसे shell में भेजना चाहिए। Read-only command भी database credentials, internal topology, environment values या customer data उजागर कर सकता है।

लोग अक्सर कहते हैं कि carefully quoted shell command पर्याप्त है क्योंकि calling agent trusted है। यह दावा तब टूट जाता है जब agent hostile repository instructions का पालन करता है, value को instruction समझ लेता है या साधारण गलती करता है। Remote server यह नहीं जान सकता कि खतरनाक request दुर्भावना से आई है या किसी अति-उत्साही tool call से। उसे केवल input दिखाई देता है। उसका निर्णय deterministic बनाएँ।

अगर structured inputs चाहिए, तो bounded format भेजें और ऐसे parser से parse करें जो extra fields अस्वीकार कर दे। JSON अपने आप सुरक्षित नहीं होता, क्योंकि shell wrapper उसे फिर भी गलत तरीके से संभाल सकता है। `release <64 lowercase hex characters>` जैसी छोटी request को validate, document, test और audit करना optional fields वाले JSON blob की तुलना में आसान है।

## Forwarding restriction की भावना को दरकिनार कर सकता है

Forced command अपने आप authenticated client को SSH को tunnel की तरह इस्तेमाल करने से नहीं रोकता। OpenSSH manual command execution और forwarding को अलग controls मानता है। अगर आप केवल `command="..."` जोड़ते हैं, तो server configuration के बाकी हिस्सों के आधार पर client sshd से local port को internal service तक forward करने का अनुरोध कर सकता है।

यह इसलिए महत्वपूर्ण है क्योंकि restricted account के पास ऐसा network access हो सकता है जो agent के पास नहीं है। Agent remote host पर `/usr/bin/ps` नहीं चला सकता, फिर भी forwarding खुली रहने पर उस host के ज़रिए database port तक पहुँच सकता है। ऐसे में account deployment identity नहीं रह जाता, बल्कि network pivot बन जाता है।

जिस account को interactive session की ज़रूरत नहीं है, उसके लिए इन सभी को बंद करें, जब तक आप यह स्पष्ट न कर सकें कि account को इनमें से किसी की ज़रूरत क्यों है:

- TCP forwarding
- agent forwarding
- X11 forwarding
- pseudo-terminal allocation
- user-controlled environment variables

आधुनिक OpenSSH deployments में `restrict` पहली चार categories संभालता है। अगर account को सचमुच किसी एक exception की ज़रूरत है, तो restriction set को पूरी तरह हटाने के बजाय उसे सीमित करें। OpenSSH `permitopen="host:port"` जैसे options support करता है, जिनसे destination forwarding सीमित की जा सकती है। इसे अलग access design मानें और allowed तथा denied दोनों destinations को test करें।

Wrapper के outbound network access की भी जाँच करें। ऐसा deployment script जो arbitrary URLs fetch कर सकती है, arbitrary repositories clone कर सकती है या arbitrary data बाहर भेज सकती है, SSH forwarding बंद होने पर भी एक व्यापक channel रखती है। Fixed artifact sources और pinned revisions यह exposure घटाते हैं। बाकी नियंत्रण के लिए firewall rules या service-specific credentials की ज़रूरत पड़ सकती है।

## Connection खुलने से पहले approval होनी चाहिए

Forced command approved SSH उपयोग से होने वाले नुकसान को सीमित करता है। यह तय नहीं करता कि मौजूदा agent process को credential का इस्तेमाल करना चाहिए या नहीं। यह निर्णय credential boundary पर, agent के SSH connection बनाने से पहले होना चाहिए।

Autonomous coding agents के लिए यह खास तौर पर महत्वपूर्ण है। Repository agent को deployment command चलाने का निर्देश दे सकती है। Tool output भी ऐसा अनुरोध कर सकता है। Compromised dependency agent को उसी दिशा में मोड़ सकती है। अगर credential agent के environment या filesystem में रखा है, तो agent उस क्षण किसी व्यक्ति को दिखाए बिना उसका इस्तेमाल कर सकता है।

SSH private keys को agent process से बाहर रखें और जब कोई नया agent run पहली बार access माँगे, तब approval लें। High-consequence accounts के लिए हर credential उपयोग पर approval लें। इसके बाद remote forced command उस action पर एक hard ceiling लगा देता है जिसे approval अधिकृत करती है।

Sallyport इस विभाजन को इस तरह लागू करता है कि SSH keys उसके encrypted vault में रहती हैं, नए agent processes को default रूप से प्रति-session authorize किया जाता है, और key agent को देने के बजाय SSH को उसके `sp-ssh` helper के ज़रिए चलाया जाता है।

Approval card को server authorization न समझें। Approval का सवाल है, «क्या यह process अभी इस credential का इस्तेमाल कर सकता है?» Server का सवाल है, «Login के बाद यह credential क्या कर सकता है?» दोनों सवालों के जवाब ज़रूरी हैं क्योंकि दोनों अलग तरह से विफल होते हैं। Approval किसी चौंकाने वाले process को रोक सकती है। Forced commands किसी approved process को release credential को shell में बदलने से रोक सकते हैं।

Approval description उपयोगी बनाएँ। Credential label में environment और action का नाम लिखें, जैसे `production release` या `staging diagnostics`। `deploy-key-2` जैसा label reviewer से interruption के दौरान पुरानी जानकारी याद रखने की अपेक्षा करता है। इसी तरह routine approvals automatic clicks में बदल जाती हैं।

## Allowlist बढ़ने से पहले deployment और diagnosis अलग करें

Deployment और diagnosis दोनों को SSH की ज़रूरत पड़ सकती है, इसलिए वे समान लगते हैं। लेकिन उनके data flows और failure modes अलग होते हैं। जहाँ संभव हो, उन्हें अलग accounts या अलग forced-command credentials के पीछे रखें।

Deployment account state बदलता है। वह fixed revision fetch कर सकता है, artifact build कर सकता है, release directory बदल सकता है और एक service restart कर सकता है। उसका output revision, target, exit status और छोटा failure message बताए। उसे arbitrary log access, process inspection या database queries की ज़रूरत नहीं है।

Diagnostic account state पढ़ता है। वह health endpoint का result, सीमित queue count, service version या सावधानी से filtered local log का tail दिखा सकता है। उसे services restart नहीं करनी चाहिए, files rotate नहीं करनी चाहिए, हर process query नहीं करना चाहिए और arbitrary paths पढ़ने नहीं चाहिए। जैसे ही diagnostic wrapper user-provided file name, unit name, host या command option स्वीकार करे, उसके input model की फिर से समीक्षा करें।

एक combined account अक्सर ऐसी निर्दोष सूची से शुरू होता है:

```text
release <revision>
health
logs <service>
restart <service>
```

फिर किसी को `logs api --since` चाहिए, किसी और को emergency restart, और wrapper `journalctl` या `systemctl` को arguments आगे भेजना शुरू कर देता है। जल्द ही code में ऐसे special cases भर जाते हैं जिन्हें कोई समझा नहीं सकता। ऐसा होने से पहले accounts अलग कर दें। अलग credentials से आप production changes के लिए अधिक सख्त approval माँग सकते हैं और कम जोखिम वाला diagnostic workflow अलग रख सकते हैं।

हर action को ऐसा record बनाना चाहिए जिसमें यह लिखा हो कि wrapper ने क्या स्वीकार किया, केवल opaque SSH command string नहीं। Release के लिए immutable revision और target name log करें। Diagnosis के लिए named diagnostic और उसका परिणाम log करें। Command arguments और logs दोनों में secret values न रखें। अगर किसी action को secret चाहिए, तो remote script उसे अपने controlled mechanism से प्राप्त करे, SSH client से स्वीकार न करे।

## Disposable client से denial paths test करें

Restricted account वास्तव में तभी restricted है जब आप उन requests को test कर लें जिन्हें उसे अस्वीकार करना है। Production environment में configuration पर भरोसा करने से पहले disposable account या test host से ये जाँचें चलाएँ। Examples मानते हैं कि credential server पर पहले से installed है।

```sh
ssh release@deploy.example "release 9f2a7c6d1e4b8a03"
ssh release@deploy.example
ssh release@deploy.example "id"
ssh release@deploy.example "release 9f2a; id"
ssh -N -L 15432:db.internal:5432 release@deploy.example
ssh -tt release@deploy.example "health"
```

पहला command तभी release wrapper तक पहुँचना चाहिए जब revision उसके नियमों पर खरी उतरे। अगले तीन commands wrapper का denial message और nonzero exit status देकर विफल होने चाहिए। Port-forwarding प्रयास listener स्थापित करने से पहले विफल होना चाहिए। Terminal request विफल होनी चाहिए या बिना terminal के चलनी चाहिए, यह इस बात पर निर्भर करेगा कि आपका client denied allocation को कैसे report करता है।

इसके बाद कम स्पष्ट cases test करें। Leading और trailing spaces, tabs, empty quoted command, newline characters, बहुत लंबा argument, Unicode whitespace, command substitutions, redirections और duplicate arguments आज़माएँ। अगर आपका shell या wrapper इनमें से किसी को normalize करके accepted request बना देता है, तो grammar को और सख्त करें।

Test के हिस्से के रूप में account की file permissions और ownership भी जाँचें। जो attacker `/usr/local/libexec/release-gate` बदल सकता है, उसे SSH bypass करने की ज़रूरत नहीं है। जो account अपने deployment source को बदल सकता है, वह अधिक privilege के साथ चलने वाले code को भी बदल सकता है। पूरी chain देखें: `authorized_keys`, sshd configuration, wrapper, deployment scripts, service definitions, writable directories और कोई भी `sudoers` entry।

## Boundary के दोनों तरफ request का audit करें

Remote logs बताते हैं कि server ने क्या स्वीकार किया। Credential-boundary logs बताते हैं कि किस local process ने connect करने की क्षमता माँगी। दोनों रखें क्योंकि कोई भी log दूसरे का सवाल नहीं बता सकता।

Remote side पर authentication success, forced wrapper का accepted operation, immutable revision या diagnostic name, request identifier और final status log करें। इन records को ऐसी जगह भेजें जिसे service account बदल न सके। Raw `SSH_ORIGINAL_COMMAND` log न करें, अगर callers उसमें secret material डाल सकते हैं। बेहतर है कि आपका protocol secret material स्वीकार ही न करे।

Local side पर session identity और individual SSH action request सुरक्षित रखें। Sallyport agent runs और individual calls को एक encrypted, hash-chained audit log से अलग journals में record करता है, और `sp audit verify` vault key के बिना offline उस chain को verify कर सकता है।

Hash chain खराब permission को अच्छी permission में नहीं बदलती। लेकिन इससे recorded history में बाद के बदलावों का पता लगाना आसान हो जाता है। Failed deployment, disputed approval या चौंकाने वाले command request के बाद यह उपयोगी है। यह एक अच्छी discipline भी बनाती है: action vocabulary पहले से तय करें, ताकि records इंसान समझ सकें।

पहला implementation उबाऊ होना चाहिए। Dedicated Unix account बनाएँ, एक forced wrapper रखें, fixed input grammar वाला एक operation रखें, forwarding बंद करें और ऐसा test लिखें जो साबित करे कि `ssh account@host` shell नहीं देता। Capabilities तभी जोड़ें जब आप उनके input, output, file access, network access और approval देने वाले व्यक्ति का नाम बता सकें।
