initial commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Sudo TTY Workaround for SSH Commands
|
||||
|
||||
When `sudo` requires a TTY (`requiretty` in sudoers) and you're running commands via SSH without a pseudo-terminal, `sudo` refuses even with `NOPASSWD` set. On newer sudo versions (Ubuntu 26.04+), `Defaults:user !requiretty` is **not a valid setting** and will cause a parse error.
|
||||
|
||||
## The Problem
|
||||
|
||||
```bash
|
||||
ssh user@host 'sudo whoami'
|
||||
# → sudo: a terminal is required to authenticate
|
||||
```
|
||||
|
||||
Even if `/etc/sudoers.d/user` contains `user ALL=(ALL) NOPASSWD: ALL`, the `requiretty` flag in the main sudoers file still blocks non-TTY commands.
|
||||
|
||||
## The Solution: Python PTY Fork
|
||||
|
||||
Use Python's `pty` module to fork a child with a proper TTY, write to its stdin, and collect the result:
|
||||
|
||||
```python
|
||||
import pty, os
|
||||
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
# child — executes the sudo command with a real TTY
|
||||
os.execvp("sudo", ["sudo", "tee", "/etc/sudoers.d/ray"])
|
||||
else:
|
||||
# parent — sends input to the child's TTY
|
||||
os.write(fd, b"<password>\n")
|
||||
os.write(fd, b"ray ALL=(ALL) NOPASSWD: ALL\n")
|
||||
os.close(fd)
|
||||
os.waitpid(pid, 0)
|
||||
print("DONE")
|
||||
```
|
||||
|
||||
**However**, this approach is fragile — `os.write()` to a PTY doesn't guarantee the sudo process reads the password before the content. It may print "DONE" without actually writing the file.
|
||||
|
||||
## The Reliable Solution: Python subprocess with `sudo -S`
|
||||
|
||||
Run this **directly on the remote machine** via SSH (NOT piped through the terminal tool's sudo filter):
|
||||
|
||||
```bash
|
||||
ssh user@host 'python3 -c "
|
||||
import subprocess
|
||||
r = subprocess.run([\"sudo\", \"-S\", \"tee\", \"/etc/sudoers.d/ray\"],
|
||||
input=b\"<password>\\nray ALL=(ALL) NOPASSWD: ALL\\n\",
|
||||
capture_output=True, timeout=10)
|
||||
print(\"RC:\", r.returncode)
|
||||
"'
|
||||
```
|
||||
|
||||
This works because `sudo -S` reads the password from stdin, and `subprocess.run()` with `input=` pipes it to the child process. The `capture_output=True` captures any password prompts.
|
||||
|
||||
## Why Other Approaches Don't Work
|
||||
|
||||
| Approach | Result |
|
||||
|----------|--------|
|
||||
| `echo 'nopasswd' \| sudo tee file` | Fails — requires TTY |
|
||||
| `script -qc "sudo tee file" /dev/null` | Prompts for password interactively, hangs |
|
||||
| `ssh -tt user@host 'sudo ...'` | Opens PTY but prompts for password, SSH tool blocks `sudo -S` |
|
||||
| `Default:user !requiretty` | Not a valid setting in sudo 1.9.x+ (Ubuntu 26.04) |
|
||||
|
||||
## Verification
|
||||
|
||||
After creating the sudoers file:
|
||||
|
||||
```bash
|
||||
sudo -n whoami
|
||||
# → root
|
||||
```
|
||||
|
||||
The `-n` (non-interactive) flag ensures sudo won't prompt for a password. If it returns `root`, passwordless sudo is working.
|
||||
|
||||
## Cleanup on Ubuntu 26.04+
|
||||
|
||||
If you accidentally added `Defaults:user !requiretty`:
|
||||
|
||||
```bash
|
||||
sudo sed -i "/requiretty/d" /etc/sudoers.d/ray
|
||||
sudo visudo -c -f /etc/sudoers.d/ray
|
||||
```
|
||||
|
||||
The `requiretty` flag was removed from sudo's parser in newer versions (saw this on Ubuntu 26.04 with sudo 1.9.x+). It causes: `unknown setting: 'requiretty'`.
|
||||
Reference in New Issue
Block a user