agent-temporary 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/bin/agent-temporary-setup.js +159 -0
- package/bin/postinstall.js +9 -0
- package/manifest.json +5 -0
- package/package.json +18 -0
- package/payload/SHA256SUMS +13 -0
- package/payload/VERSION +1 -0
- package/payload/agent-temporary +327 -0
- package/payload/agent-temporary-boot.openrc +10 -0
- package/payload/agent-temporary-boot.service +11 -0
- package/payload/agent-temporary-expire.openrc +15 -0
- package/payload/agent-temporary-expire.service +6 -0
- package/payload/agent-temporary-expire.timer +11 -0
- package/payload/install.sh +4 -0
- package/payload/macos/agent-temporary-macos +292 -0
- package/payload/macos/agent-temporary-reaper +2 -0
- package/payload/macos/com.agent-temporary.boot.plist +22 -0
- package/payload/macos/com.agent-temporary.expire.plist +15 -0
- package/payload/uninstall.sh +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# agent-temporary
|
|
2
|
+
|
|
3
|
+
agent-temporary 0.7.0 — small utility for explicitly bounded temporary root access on Linux or macOS.
|
|
4
|
+
|
|
5
|
+
## Contract
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
sudo agent-temporary install --user zero
|
|
9
|
+
sudo agent-temporary on
|
|
10
|
+
sudo agent-temporary on --ttl 30m
|
|
11
|
+
sudo agent-temporary on --ttl 2h --persist-reboot
|
|
12
|
+
sudo agent-temporary off
|
|
13
|
+
agent-temporary status
|
|
14
|
+
agent-temporary version
|
|
15
|
+
sudo ./uninstall.sh
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The npm distribution installs only an unprivileged setup command:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install -g agent-temporary
|
|
22
|
+
agent-temporary-setup install
|
|
23
|
+
agent-temporary status
|
|
24
|
+
agent-temporary-setup update
|
|
25
|
+
npm uninstall -g agent-temporary
|
|
26
|
+
agent-temporary-setup uninstall-system
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`npm install` and `npm uninstall` affect only the npm package. The explicit setup
|
|
30
|
+
commands invoke the existing system installer or uninstaller and request administrator
|
|
31
|
+
privileges; they do not activate temporary access. To update the setup package, use
|
|
32
|
+
`npm install -g agent-temporary@latest` and then run `agent-temporary-setup update`.
|
|
33
|
+
|
|
34
|
+
Activation defaults to a 5-minute TTL. The allowed range is 5 minutes through 8 hours;
|
|
35
|
+
accepted forms are integer minutes or hours such as `30m`, `1h`, and `4h`.
|
|
36
|
+
|
|
37
|
+
On macOS, `--persist-reboot` explicitly preserves the same grant across reboot
|
|
38
|
+
until its original expiry. On Linux, the existing behavior remains unchanged.
|
|
39
|
+
Without persistence, boot revocation removes
|
|
40
|
+
temporary privilege.
|
|
41
|
+
|
|
42
|
+
While active, the configured user has unrestricted `NOPASSWD: ALL` sudo access.
|
|
43
|
+
The privilege is bounded by a local expiry supervisor and is revoked on boot.
|
|
44
|
+
It does not use SSH keys or `authorized_keys`, and does not require Netbot or a
|
|
45
|
+
remote controller to revoke access.
|
|
46
|
+
|
|
47
|
+
`status` reports state from local state/rule inspection and an exact harmless
|
|
48
|
+
`sudo -n /usr/bin/true` execution probe; sudo policy listing alone is not treated
|
|
49
|
+
as effective authority. `status --json` is accepted for machine-readable integration
|
|
50
|
+
(the key/value fields remain stable). State is stored root-owned under
|
|
51
|
+
`/var/lib/agent-temporary/`.
|
|
52
|
+
|
|
53
|
+
## Platform
|
|
54
|
+
|
|
55
|
+
Supported: Linux with an active systemd or OpenRC runtime and sudo/visudo, or
|
|
56
|
+
macOS with launchd and sudo/visudo. OpenRC is detected through its native runtime
|
|
57
|
+
state and service tools, including their standard `/sbin` locations.
|
|
58
|
+
|
|
59
|
+
Unsupported: Linux with another/unknown init system and unsupported platforms;
|
|
60
|
+
they fail closed and are not given a synthetic systemd setup.
|
|
61
|
+
|
|
62
|
+
## Release
|
|
63
|
+
|
|
64
|
+
Build a release artifact with:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
./release.sh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The resulting directory contains the executable, systemd/OpenRC/launchd service definitions, `VERSION`,
|
|
71
|
+
`SHA256SUMS`, and a deterministic `install.sh`. It also creates
|
|
72
|
+
`dist/agent-temporary-X.Y.Z.zip` and its SHA-256 sidecar. The archive contains no Git metadata,
|
|
73
|
+
runtime state, logs, or private material; after extraction, run
|
|
74
|
+
`sudo ./install.sh --user zero` and the extracted source directory may be removed.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const cp = require('child_process');
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
10
|
+
const pkg = require(path.join(packageRoot, 'package.json'));
|
|
11
|
+
const payload = path.join(packageRoot, 'payload');
|
|
12
|
+
const manifestPath = path.join(packageRoot, 'manifest.json');
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
process.stderr.write(`agent-temporary-setup: ${message}\n`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function usage() {
|
|
20
|
+
process.stdout.write('Usage:\n' +
|
|
21
|
+
' agent-temporary-setup install\n' +
|
|
22
|
+
' agent-temporary-setup update\n' +
|
|
23
|
+
' agent-temporary-setup status\n' +
|
|
24
|
+
' agent-temporary-setup uninstall-system\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function versionParts(value) {
|
|
28
|
+
const match = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(value || '');
|
|
29
|
+
return match ? match.slice(1).map(Number) : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compareVersions(left, right) {
|
|
33
|
+
const a = versionParts(left);
|
|
34
|
+
const b = versionParts(right);
|
|
35
|
+
if (!a || !b) return null;
|
|
36
|
+
for (let i = 0; i < 3; i += 1) {
|
|
37
|
+
if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function verifyPayload() {
|
|
43
|
+
let manifest;
|
|
44
|
+
try {
|
|
45
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`cannot read payload manifest: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
if (manifest.version !== pkg.version || manifest.payload_version !== pkg.version) {
|
|
50
|
+
throw new Error('package version and embedded payload version do not match');
|
|
51
|
+
}
|
|
52
|
+
const version = fs.readFileSync(path.join(payload, 'VERSION'), 'utf8').trim();
|
|
53
|
+
if (version !== pkg.version) throw new Error('embedded VERSION does not match package version');
|
|
54
|
+
const checksumFile = path.join(packageRoot, manifest.payload_checksums || 'payload/SHA256SUMS');
|
|
55
|
+
const lines = fs.readFileSync(checksumFile, 'utf8').trim().split(/\n/).filter(Boolean);
|
|
56
|
+
if (!lines.length) throw new Error('embedded payload checksums are empty');
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line);
|
|
59
|
+
if (!match) throw new Error(`malformed payload checksum line: ${line}`);
|
|
60
|
+
const relative = match[2];
|
|
61
|
+
const resolved = path.resolve(payload, relative);
|
|
62
|
+
if (resolved !== payload && !resolved.startsWith(`${payload}${path.sep}`)) {
|
|
63
|
+
throw new Error(`payload checksum escapes payload: ${relative}`);
|
|
64
|
+
}
|
|
65
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(resolved)).digest('hex');
|
|
66
|
+
if (actual !== match[1]) throw new Error(`payload integrity check failed: ${relative}`);
|
|
67
|
+
}
|
|
68
|
+
for (const required of ['install.sh', 'uninstall.sh', 'agent-temporary', 'VERSION']) {
|
|
69
|
+
if (!fs.existsSync(path.join(payload, required))) throw new Error(`payload is missing ${required}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function installedSystem() {
|
|
74
|
+
const result = cp.spawnSync('agent-temporary', ['version'], { encoding: 'utf8' });
|
|
75
|
+
if (result.error || result.status !== 0) return { present: false, version: null };
|
|
76
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
77
|
+
const match = /agent-temporary\s+([^\s]+)/.exec(output);
|
|
78
|
+
return { present: true, version: match ? match[1] : null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runSudo(args) {
|
|
82
|
+
return cp.spawnSync('sudo', args, { stdio: 'inherit' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function verifyInactive() {
|
|
86
|
+
const result = cp.spawnSync('sudo', ['agent-temporary', 'status'], { encoding: 'utf8' });
|
|
87
|
+
if (result.error || result.status !== 0) throw new Error('could not verify final system status');
|
|
88
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
89
|
+
process.stdout.write(output);
|
|
90
|
+
if (!/^state=inactive$/m.test(output) || !/^effective_authority=false$/m.test(output)) {
|
|
91
|
+
throw new Error('system installation is not inactive after setup');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function doStatus() {
|
|
96
|
+
const system = installedSystem();
|
|
97
|
+
process.stdout.write(`npm/package version=${pkg.version}\n`);
|
|
98
|
+
process.stdout.write(`embedded payload version=${pkg.version}\n`);
|
|
99
|
+
process.stdout.write(`system components=${system.present ? 'present' : 'absent/unknown'}\n`);
|
|
100
|
+
process.stdout.write(`installed system version=${system.version || 'unknown'}\n`);
|
|
101
|
+
if (!system.present) process.stdout.write('update available=no (system installation not detected)\n');
|
|
102
|
+
else {
|
|
103
|
+
const comparison = compareVersions(system.version, pkg.version);
|
|
104
|
+
if (comparison === null) process.stdout.write('update available=unknown\n');
|
|
105
|
+
else if (comparison < 0) process.stdout.write('update available=yes\n');
|
|
106
|
+
else process.stdout.write('update available=no\n');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installOrUpdate(command) {
|
|
111
|
+
verifyPayload();
|
|
112
|
+
const system = installedSystem();
|
|
113
|
+
const comparison = system.version ? compareVersions(system.version, pkg.version) : null;
|
|
114
|
+
if (system.present && comparison === null) throw new Error('installed system version is unknown; refusing ambiguous installation');
|
|
115
|
+
if (comparison > 0) throw new Error(`installed system version ${system.version} is newer than package version ${pkg.version}; downgrade is not supported`);
|
|
116
|
+
if (command === 'update' && !system.present) throw new Error('no installed system detected; use install');
|
|
117
|
+
if (comparison === 0) {
|
|
118
|
+
process.stdout.write(`agent-temporary ${pkg.version} is already current; no system changes made.\n`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write(`Installing agent-temporary ${pkg.version} system components.\nAdministrator privileges are required.\n`);
|
|
122
|
+
const result = runSudo(['sh', path.join(payload, 'install.sh')]);
|
|
123
|
+
if (result.error || result.status !== 0) throw new Error('system installer failed');
|
|
124
|
+
const installed = installedSystem();
|
|
125
|
+
if (!installed.present || installed.version !== pkg.version) throw new Error('installed system version verification failed');
|
|
126
|
+
verifyInactive();
|
|
127
|
+
process.stdout.write(`agent-temporary ${pkg.version} system installation is present and inactive.\n`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function uninstallSystem() {
|
|
131
|
+
verifyPayload();
|
|
132
|
+
process.stdout.write('Removing agent-temporary system components. Administrator privileges are required.\n');
|
|
133
|
+
const result = runSudo(['sh', path.join(payload, 'uninstall.sh')]);
|
|
134
|
+
if (result.error || result.status !== 0) throw new Error('system uninstaller failed');
|
|
135
|
+
const system = installedSystem();
|
|
136
|
+
if (system.present) throw new Error('system command remains after uninstall');
|
|
137
|
+
process.stdout.write('agent-temporary system installation removed.\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main() {
|
|
141
|
+
const command = process.argv[2];
|
|
142
|
+
if (!command) return usage();
|
|
143
|
+
if (command === '--help' || command === '-h') return usage();
|
|
144
|
+
if (command === '--version') return process.stdout.write(`agent-temporary-setup ${pkg.version}\n`);
|
|
145
|
+
if (!['status', 'install', 'update', 'uninstall-system'].includes(command)) {
|
|
146
|
+
usage();
|
|
147
|
+
process.exitCode = 2;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (command === 'status') doStatus();
|
|
152
|
+
else if (command === 'uninstall-system') uninstallSystem();
|
|
153
|
+
else installOrUpdate(command);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fail(error.message);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
process.stdout.write(`agent-temporary ${pkg.version} installed.\n\n` +
|
|
6
|
+
'System components are not installed automatically.\n' +
|
|
7
|
+
'To finish setup:\n' +
|
|
8
|
+
' agent-temporary-setup install\n\n' +
|
|
9
|
+
'This step will request administrator privileges.\n');
|
package/manifest.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-temporary",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Unprivileged setup layer for the agent-temporary system release",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agent-temporary-setup": "bin/agent-temporary-setup.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"payload",
|
|
11
|
+
"manifest.json",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node bin/postinstall.js"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
a41b716bf984f50311a67f6841b624741f534d80bfb7ca9d7e68e2b68a192882 agent-temporary
|
|
2
|
+
661adb81432a930663d85f3f7a36bff5a6bdc82534012688b3ff34502d792612 agent-temporary-expire.service
|
|
3
|
+
c14e46956698ac589515971fa71171034f94ee0276c97645c363b0729c1d2870 agent-temporary-expire.timer
|
|
4
|
+
425bc4e3601631c6cf612140905a5e8f1167320278c870cb5d4b6723eadda5e6 agent-temporary-boot.service
|
|
5
|
+
fc2cab450fa5743ba05b46ab8a5aca8d8e418ba39517be52848630a05dda9eb0 agent-temporary-boot.openrc
|
|
6
|
+
15369ed62ff24f98c19d3d331f32b12f1d0712b5e3cbac9c2cddb5ad22c2e00a agent-temporary-expire.openrc
|
|
7
|
+
d26cd02487363aaa2f633f6c83bd61206fed2297004023b0c3e143c20633f416 macos/agent-temporary-macos
|
|
8
|
+
1dff1a00db27128e2c618877fd2c76986bc27240c1b5b075a9daa3b8b9d58fce macos/agent-temporary-reaper
|
|
9
|
+
0cf3a987694cc296331cbbeee0ef3af77dd1e21f1136f5438b5a621b6fe75ce5 macos/com.agent-temporary.expire.plist
|
|
10
|
+
e963a4d32c7b47eb68db3e286ca53675b59ddd22370ad330334115e32a877363 macos/com.agent-temporary.boot.plist
|
|
11
|
+
967d9afb101346667166f2e76e81910bc190488d7d41d50ca0072e9d92f00e32 VERSION
|
|
12
|
+
2ee46662f8db2fb0d6536b91cef0aad4239255d91f9485de7b34111125a8ccd7 install.sh
|
|
13
|
+
a52fb0bd6dd7c24069096706c075b35fc511468912970bb6eaadb2bcdc83a32d uninstall.sh
|
package/payload/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.7.0
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
VERSION=0.7.0
|
|
5
|
+
DEFAULT_TTL=5m
|
|
6
|
+
PREFIX=${PREFIX:-/usr/local/bin}
|
|
7
|
+
TARGET=${TARGET:-$PREFIX/agent-temporary}
|
|
8
|
+
RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-temporary-agent}
|
|
9
|
+
CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
|
|
10
|
+
STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/lib/agent-temporary}
|
|
11
|
+
STATE=${AGENT_TEMPORARY_STATE:-$STATE_DIR/state}
|
|
12
|
+
SYSTEMD_DIR=${AGENT_TEMPORARY_SYSTEMD_DIR:-/etc/systemd/system}
|
|
13
|
+
SYSTEMCTL=${SYSTEMCTL:-systemctl}
|
|
14
|
+
OPENRC_DIR=${AGENT_TEMPORARY_OPENRC_DIR:-/etc/init.d}
|
|
15
|
+
RCSERVICE=${RCSERVICE:-}
|
|
16
|
+
RCUPDATE=${RCUPDATE:-}
|
|
17
|
+
OPENRC_PIDFILE=${AGENT_TEMPORARY_OPENRC_PIDFILE:-/run/agent-temporary-expire.pid}
|
|
18
|
+
VISUDO=${VISUDO:-visudo}
|
|
19
|
+
SUDOERS=${AGENT_TEMPORARY_SUDOERS:-/etc/sudoers}
|
|
20
|
+
EXPIRY_TIMER=agent-temporary-expire.timer
|
|
21
|
+
SOURCE_DIR=${AGENT_TEMPORARY_RELEASE_DIR:-.}
|
|
22
|
+
UNAME=${UNAME:-uname}
|
|
23
|
+
|
|
24
|
+
OS=$($UNAME -s 2>/dev/null || true)
|
|
25
|
+
|
|
26
|
+
usage() { echo "Usage: sudo $0 {install --user USER|on --ttl DURATION|off|status [--json]|version}" >&2; exit 2; }
|
|
27
|
+
need_root() { [ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0 $*" >&2; exit 1; }; }
|
|
28
|
+
die() { echo "agent-temporary: $*" >&2; exit 1; }
|
|
29
|
+
version() { echo "agent-temporary $VERSION"; }
|
|
30
|
+
|
|
31
|
+
load_user() {
|
|
32
|
+
[ -r "$CONFIG" ] || die "not installed: $CONFIG is missing"
|
|
33
|
+
. "$CONFIG"
|
|
34
|
+
[ -n "${AGENT_USER:-}" ] || die "invalid config: AGENT_USER is missing"
|
|
35
|
+
id "$AGENT_USER" >/dev/null 2>&1 || die "unknown configured user: $AGENT_USER"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validate_platform() {
|
|
39
|
+
[ "$OS" = Linux ] || die "Linux systemd or OpenRC is required"
|
|
40
|
+
command -v "$VISUDO" >/dev/null 2>&1 || die "visudo is required"
|
|
41
|
+
if command -v "$SYSTEMCTL" >/dev/null 2>&1 && { [ -d /run/systemd/system ] || "$SYSTEMCTL" show-environment >/dev/null 2>&1; }; then
|
|
42
|
+
INIT_SYSTEM=systemd
|
|
43
|
+
else
|
|
44
|
+
[ -n "$RCSERVICE" ] || for candidate in /sbin/rc-service /usr/sbin/rc-service; do [ -x "$candidate" ] && { RCSERVICE=$candidate; break; }; done
|
|
45
|
+
[ -n "$RCUPDATE" ] || for candidate in /sbin/rc-update /usr/sbin/rc-update; do [ -x "$candidate" ] && { RCUPDATE=$candidate; break; }; done
|
|
46
|
+
[ -x "${RCSERVICE:-/nonexistent}" ] && [ -x "${RCUPDATE:-/nonexistent}" ] || die "Linux systemd or OpenRC is required"
|
|
47
|
+
[ -e /run/openrc/softlevel ] || die "OpenRC is not active"
|
|
48
|
+
INIT_SYSTEM=openrc
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
service_unit() { [ "$1" = agent-temporary-expire ] && [ "$INIT_SYSTEM" = systemd ] && echo agent-temporary-expire.timer || echo "$1"; }
|
|
53
|
+
service_start() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" start; else "$SYSTEMCTL" start "$unit"; fi; }
|
|
54
|
+
service_stop() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" stop >/dev/null 2>&1 || true; else "$SYSTEMCTL" stop "$unit" >/dev/null 2>&1 || true; fi; }
|
|
55
|
+
service_active() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" status >/dev/null 2>&1; else "$SYSTEMCTL" is-active --quiet "$unit"; fi; }
|
|
56
|
+
|
|
57
|
+
parse_ttl() {
|
|
58
|
+
ttl=$1
|
|
59
|
+
case "$ttl" in
|
|
60
|
+
[0-9]*m) number=${ttl%m}; seconds=$((number * 60)) ;;
|
|
61
|
+
[0-9]*h) number=${ttl%h}; seconds=$((number * 3600)) ;;
|
|
62
|
+
*) die "TTL must be an integer number of minutes or hours (30m or 1h)" ;;
|
|
63
|
+
esac
|
|
64
|
+
case "$number" in ''|*[!0-9]*) die "malformed TTL: $ttl" ;; esac
|
|
65
|
+
[ "$seconds" -ge 300 ] || die "TTL minimum is 5m"
|
|
66
|
+
[ "$seconds" -le 28800 ] || die "TTL maximum is 8h"
|
|
67
|
+
echo "$seconds"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
validate_sudoers() { "$VISUDO" -cf "$SUDOERS" >/dev/null; }
|
|
71
|
+
|
|
72
|
+
effective_unrestricted() {
|
|
73
|
+
# sudo -l describes policy, not effective execution. Probe the exact
|
|
74
|
+
# noninteractive mechanism with a harmless command as the configured
|
|
75
|
+
# user. This also catches Defaults/authenticate and group-rule ordering
|
|
76
|
+
# differences across sudo implementations.
|
|
77
|
+
"$VISUDO" -cf "$SUDOERS" >/dev/null 2>&1 || return 1
|
|
78
|
+
true_command=
|
|
79
|
+
for candidate in /usr/bin/true /bin/true; do
|
|
80
|
+
if [ -x "$candidate" ]; then true_command=$candidate; break; fi
|
|
81
|
+
done
|
|
82
|
+
[ -n "$true_command" ] || return 1
|
|
83
|
+
if [ "$(id -u)" -eq 0 ]; then
|
|
84
|
+
su -s /bin/sh "$1" -c "sudo -n $true_command" >/dev/null 2>&1
|
|
85
|
+
elif [ "$(id -un)" = "$1" ]; then
|
|
86
|
+
sudo -n "$true_command" >/dev/null 2>&1
|
|
87
|
+
else
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
write_rule() {
|
|
93
|
+
tmp=$(mktemp "${RULE}.XXXXXX")
|
|
94
|
+
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
95
|
+
# Group rules (notably Alpine's wheel rule) can match after the user
|
|
96
|
+
# specification and restore password authentication. The scoped
|
|
97
|
+
# Defaults entry makes the temporary NOPASSWD contract effective for the
|
|
98
|
+
# configured user while this file exists; it disappears on off/expiry.
|
|
99
|
+
printf 'Defaults:%s !authenticate\n%s ALL=(ALL) NOPASSWD: ALL\n' "$AGENT_USER" "$AGENT_USER" > "$tmp"
|
|
100
|
+
chmod 440 "$tmp"; chown root:"$(id -gn root)" "$tmp"
|
|
101
|
+
"$VISUDO" -cf "$tmp" >/dev/null
|
|
102
|
+
mv -f "$tmp" "$RULE"
|
|
103
|
+
trap - EXIT HUP INT TERM
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
write_state() {
|
|
107
|
+
activated_epoch=$1; expires_epoch=$2; ttl=$3; persist_reboot=$4
|
|
108
|
+
tmp=$(mktemp "${STATE}.XXXXXX")
|
|
109
|
+
trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
110
|
+
{
|
|
111
|
+
printf 'version=%s\n' "$VERSION"
|
|
112
|
+
printf 'user=%s\n' "$AGENT_USER"
|
|
113
|
+
printf 'activated_at_epoch=%s\n' "$activated_epoch"
|
|
114
|
+
printf 'expires_at_epoch=%s\n' "$expires_epoch"
|
|
115
|
+
printf 'ttl=%s\n' "$ttl"
|
|
116
|
+
printf 'persist_reboot=%s\n' "$persist_reboot"
|
|
117
|
+
} > "$tmp"
|
|
118
|
+
chmod 600 "$tmp"; chown root:"$(id -gn root)" "$tmp"; mv -f "$tmp" "$STATE"
|
|
119
|
+
trap - EXIT HUP INT TERM
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
state_value() { sed -n "s/^$1=//p" "$STATE" | head -n 1; }
|
|
123
|
+
|
|
124
|
+
install_units() {
|
|
125
|
+
if [ "${INIT_SYSTEM:-}" = openrc ]; then
|
|
126
|
+
install -d -m 755 "$OPENRC_DIR"
|
|
127
|
+
install -o root -g "$(id -gn root)" -m 755 "$SOURCE_DIR/agent-temporary-boot.openrc" "$OPENRC_DIR/agent-temporary-boot"
|
|
128
|
+
install -o root -g "$(id -gn root)" -m 755 "$SOURCE_DIR/agent-temporary-expire.openrc" "$OPENRC_DIR/agent-temporary-expire"
|
|
129
|
+
"$RCUPDATE" add agent-temporary-boot default >/dev/null 2>&1 || true
|
|
130
|
+
"$RCUPDATE" add agent-temporary-expire default >/dev/null 2>&1 || true
|
|
131
|
+
return
|
|
132
|
+
fi
|
|
133
|
+
install -d -m 755 "$SYSTEMD_DIR"
|
|
134
|
+
install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-expire.service" "$SYSTEMD_DIR/agent-temporary-expire.service"
|
|
135
|
+
install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-expire.timer" "$SYSTEMD_DIR/agent-temporary-expire.timer"
|
|
136
|
+
install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-boot.service" "$SYSTEMD_DIR/agent-temporary-boot.service"
|
|
137
|
+
"$SYSTEMCTL" daemon-reload
|
|
138
|
+
"$SYSTEMCTL" enable agent-temporary-boot.service >/dev/null
|
|
139
|
+
"$SYSTEMCTL" enable agent-temporary-expire.timer >/dev/null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
do_install() {
|
|
143
|
+
need_root install; validate_platform
|
|
144
|
+
target_user=${INSTALL_USER:-${AGENT_USER:-${SUDO_USER:-}}}
|
|
145
|
+
[ -n "$target_user" ] || die "set AGENT_USER or run through sudo from the target user"
|
|
146
|
+
id "$target_user" >/dev/null 2>&1 || die "unknown user: $target_user"
|
|
147
|
+
install -d -m 755 "$PREFIX"; install -d -m 700 "$STATE_DIR"; install -d -m 700 "$STATE_DIR/backups"
|
|
148
|
+
stamp=$(date +%Y%m%d-%H%M%S); backup="$STATE_DIR/backups/$stamp"; install -d -m 700 "$backup"
|
|
149
|
+
for file in "$RULE" "$TARGET" "$CONFIG"; do [ -e "$file" ] && cp -p "$file" "$backup/$(basename "$file")" || true; done
|
|
150
|
+
rm -f "$RULE" "$STATE"
|
|
151
|
+
install -o root -g "$(id -gn root)" -m 755 "$0" "$TARGET"
|
|
152
|
+
tmp=$(mktemp "${CONFIG}.XXXXXX"); trap 'rm -f "$tmp"' EXIT HUP INT TERM
|
|
153
|
+
printf 'AGENT_USER=%s\n' "$target_user" > "$tmp"
|
|
154
|
+
chmod 600 "$tmp"; chown root:"$(id -gn root)" "$tmp"; mv -f "$tmp" "$CONFIG"
|
|
155
|
+
trap - EXIT HUP INT TERM
|
|
156
|
+
install_units
|
|
157
|
+
service_stop agent-temporary-expire
|
|
158
|
+
validate_sudoers
|
|
159
|
+
echo "Installed $TARGET version $VERSION for user $target_user; temporary access is inactive."
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
macos_plist_lint() {
|
|
163
|
+
command -v plutil >/dev/null 2>&1 || die "plutil is required on macOS"
|
|
164
|
+
plutil -lint "$1" >/dev/null || die "invalid launchd plist: $1"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
macos_install_file() {
|
|
168
|
+
src=$1 dest=$2 mode=$3
|
|
169
|
+
[ -f "$src" ] || die "release file is missing: $src"
|
|
170
|
+
install -o root -g wheel -m "$mode" "$src" "$dest" || die "cannot install $dest"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
do_install_macos() {
|
|
174
|
+
need_root install
|
|
175
|
+
command -v visudo >/dev/null 2>&1 || die "visudo is required"
|
|
176
|
+
[ ! -e /etc/sudoers.d/90-temporary-agent ] || die "legacy sudoers fragment exists; remove it before installation"
|
|
177
|
+
target_user=${INSTALL_USER:-${SUDO_USER:-}}
|
|
178
|
+
[ -n "$target_user" ] || die "set --user USER or run through sudo from the target user"
|
|
179
|
+
id "$target_user" >/dev/null 2>&1 || die "unknown user: $target_user"
|
|
180
|
+
macos_backend=$SOURCE_DIR/macos/agent-temporary-macos
|
|
181
|
+
macos_reaper=$SOURCE_DIR/macos/agent-temporary-reaper
|
|
182
|
+
macos_expire=$SOURCE_DIR/macos/com.agent-temporary.expire.plist
|
|
183
|
+
macos_boot=$SOURCE_DIR/macos/com.agent-temporary.boot.plist
|
|
184
|
+
for plist in "$macos_expire" "$macos_boot"; do macos_plist_lint "$plist"; done
|
|
185
|
+
sh -n "$macos_backend" "$macos_reaper"
|
|
186
|
+
if [ -e /etc/sudoers.d/90-agent-temporary ] || [ -e /var/db/agent-temporary/state ]; then
|
|
187
|
+
[ -x /usr/local/libexec/agent-temporary-macos ] || die "managed macOS authority exists but its backend is missing"
|
|
188
|
+
/usr/local/libexec/agent-temporary-macos off || die "cannot revoke existing macOS authority"
|
|
189
|
+
fi
|
|
190
|
+
"$VISUDO" -cf "$SUDOERS" >/dev/null || die "sudoers validation failed before installation"
|
|
191
|
+
install -d -o root -g wheel -m 755 /usr/local/bin /usr/local/libexec
|
|
192
|
+
install -d -o root -g wheel -m 700 /var/db/agent-temporary
|
|
193
|
+
macos_install_file "$macos_backend" /usr/local/libexec/agent-temporary-macos 755
|
|
194
|
+
macos_install_file "$macos_reaper" /usr/local/libexec/agent-temporary-reaper 755
|
|
195
|
+
macos_install_file "$macos_expire" /Library/LaunchDaemons/com.agent-temporary.expire.plist 644
|
|
196
|
+
macos_install_file "$macos_boot" /Library/LaunchDaemons/com.agent-temporary.boot.plist 644
|
|
197
|
+
tmp=$(mktemp /etc/agent-temporary.conf.XXXXXX)
|
|
198
|
+
printf 'CONFIG_USER=%s\n' "$target_user" > "$tmp"
|
|
199
|
+
chmod 600 "$tmp"; chown root:wheel "$tmp"; mv -f "$tmp" /etc/agent-temporary.conf
|
|
200
|
+
macos_install_file "$SOURCE_DIR/agent-temporary" "$TARGET" 755
|
|
201
|
+
launchctl bootout system/com.agent-temporary.expire >/dev/null 2>&1 || true
|
|
202
|
+
launchctl bootout system/com.agent-temporary.boot >/dev/null 2>&1 || true
|
|
203
|
+
launchctl bootstrap system /Library/LaunchDaemons/com.agent-temporary.expire.plist || die "cannot bootstrap expiry launchd job"
|
|
204
|
+
launchctl bootstrap system /Library/LaunchDaemons/com.agent-temporary.boot.plist || die "cannot bootstrap boot launchd job"
|
|
205
|
+
launchctl print system/com.agent-temporary.expire >/dev/null 2>&1 || die "expiry launchd job is not loaded"
|
|
206
|
+
launchctl print system/com.agent-temporary.boot >/dev/null 2>&1 || die "boot launchd job is not loaded"
|
|
207
|
+
"$VISUDO" -cf "$SUDOERS" >/dev/null || die "sudoers validation failed after installation"
|
|
208
|
+
echo "Installed $TARGET version $VERSION for user $target_user; temporary access is inactive."
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
do_on() {
|
|
212
|
+
need_root on
|
|
213
|
+
ttl_text=; persist_reboot=no
|
|
214
|
+
while [ "$#" -gt 0 ]; do
|
|
215
|
+
case "$1" in
|
|
216
|
+
--ttl) [ "$#" -ge 2 ] || die "--ttl requires a duration"; [ -z "$ttl_text" ] || die "--ttl supplied more than once"; ttl_text=$2; shift 2 ;;
|
|
217
|
+
--persist-reboot) [ "$persist_reboot" = no ] || die "--persist-reboot supplied more than once"; persist_reboot=yes; shift ;;
|
|
218
|
+
*) die "unknown on option: $1" ;;
|
|
219
|
+
esac
|
|
220
|
+
done
|
|
221
|
+
[ "$persist_reboot" = no ] || [ -n "$ttl_text" ] || die "--persist-reboot requires explicit --ttl DURATION"
|
|
222
|
+
[ -n "$ttl_text" ] || ttl_text=$DEFAULT_TTL
|
|
223
|
+
ttl_seconds=$(parse_ttl "$ttl_text"); validate_platform; load_user
|
|
224
|
+
if [ -f "$STATE" ]; then
|
|
225
|
+
expires=$(state_value expires_at_epoch); now=$(date +%s)
|
|
226
|
+
if [ -n "$expires" ] && [ "$now" -lt "$expires" ] && effective_unrestricted "$AGENT_USER"; then
|
|
227
|
+
echo "already active; expires_at_epoch=$expires; no changes made."; exit 0
|
|
228
|
+
fi
|
|
229
|
+
if [ -n "$expires" ] && [ "$now" -ge "$expires" ] && [ ! -e "$RULE" ]; then
|
|
230
|
+
rm -f "$STATE"
|
|
231
|
+
else
|
|
232
|
+
die "already active or inconsistent: run status/off"
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
now=$(date +%s); expires=$((now + ttl_seconds)); write_rule; write_state "$now" "$expires" "$ttl_text" "$persist_reboot"
|
|
236
|
+
if ! service_start agent-temporary-expire >/dev/null 2>&1; then rm -f "$RULE" "$STATE"; die "could not arm local expiry timer; privilege was revoked"; fi
|
|
237
|
+
validate_sudoers; echo "Temporary agent sudo access enabled for $AGENT_USER until epoch $expires."
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
do_off() {
|
|
241
|
+
need_root off; load_user; rm -f "$RULE" "$STATE"
|
|
242
|
+
service_stop agent-temporary-expire
|
|
243
|
+
validate_sudoers
|
|
244
|
+
if effective_unrestricted "$AGENT_USER"; then die "effective temporary privilege remains after revoke"; fi
|
|
245
|
+
echo "Temporary agent sudo access disabled."
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
do_expire() {
|
|
249
|
+
need_root internal; [ -f "$STATE" ] || exit 0; expires=$(state_value expires_at_epoch)
|
|
250
|
+
case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; exit 0 ;; esac
|
|
251
|
+
[ "$(date +%s)" -ge "$expires" ] || exit 0; rm -f "$RULE"; validate_sudoers
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
do_expire_loop() {
|
|
255
|
+
need_root internal
|
|
256
|
+
while [ -f "$STATE" ]; do
|
|
257
|
+
expires=$(state_value expires_at_epoch); now=$(date +%s)
|
|
258
|
+
case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; validate_sudoers; exit 0 ;; esac
|
|
259
|
+
if [ "$now" -ge "$expires" ]; then rm -f "$RULE"; validate_sudoers; exit 0; fi
|
|
260
|
+
sleep 1
|
|
261
|
+
done
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
do_boot_revoke() {
|
|
265
|
+
need_root internal
|
|
266
|
+
if [ ! -f "$STATE" ]; then rm -f "$RULE"; validate_sudoers; exit 0; fi
|
|
267
|
+
load_user; expires=$(state_value expires_at_epoch); now=$(date +%s); persist=$(state_value persist_reboot)
|
|
268
|
+
case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; validate_sudoers; exit 0 ;; esac
|
|
269
|
+
if [ "$now" -ge "$expires" ]; then rm -f "$RULE" "$STATE"; service_stop agent-temporary-expire; validate_sudoers; exit 0; fi
|
|
270
|
+
if [ "$persist" != yes ]; then rm -f "$RULE" "$STATE"; service_stop agent-temporary-expire; validate_sudoers; exit 0; fi
|
|
271
|
+
if ! effective_unrestricted "$AGENT_USER"; then write_rule; fi
|
|
272
|
+
service_start agent-temporary-expire >/dev/null
|
|
273
|
+
validate_sudoers
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
do_status() {
|
|
277
|
+
if [ "$(id -u)" -ne 0 ] && { [ ! -r "$STATE" ] || [ ! -r "$RULE" ]; }; then
|
|
278
|
+
echo "status=inspection-requires-privilege"; echo "detail=root privileges are required to inspect effective temporary sudo state"; exit 0
|
|
279
|
+
fi
|
|
280
|
+
if [ ! -r "$CONFIG" ]; then echo "status=inspection-requires-privilege"; exit 0; fi
|
|
281
|
+
. "$CONFIG"; AGENT_USER=${AGENT_USER:-unknown}; rule_present=no; [ -e "$RULE" ] && rule_present=yes; state_present=no; [ -e "$STATE" ] && state_present=yes
|
|
282
|
+
if [ "$rule_present" = no ] && [ "$state_present" = no ]; then echo "state=inactive"; echo "effective_authority=false"; echo "user=$AGENT_USER"; echo "persist_reboot=no"; echo "version=$VERSION"; exit 0; fi
|
|
283
|
+
if [ "$rule_present" = yes ] && [ "$state_present" = no ]; then echo "state=inconsistent"; echo "effective_authority=unknown"; echo "status=broken/inconsistent"; echo "detail=effective rule without state"; exit 0; fi
|
|
284
|
+
if [ "$rule_present" = no ] && [ "$state_present" = yes ]; then
|
|
285
|
+
expires=$(state_value expires_at_epoch); now=$(date +%s)
|
|
286
|
+
if [ -n "$expires" ] && [ "$now" -ge "$expires" ]; then echo "state=expired"; echo "effective_authority=false"; else echo "state=inconsistent"; echo "effective_authority=false"; echo "status=broken/inconsistent"; echo "detail=state without effective rule"; fi
|
|
287
|
+
exit 0
|
|
288
|
+
fi
|
|
289
|
+
expires=$(state_value expires_at_epoch); now=$(date +%s)
|
|
290
|
+
if [ -z "$expires" ]; then echo "state=inconsistent"; echo "effective_authority=unknown"; echo "status=broken/inconsistent"; echo "detail=missing expiry"; exit 0; fi
|
|
291
|
+
if [ "$now" -ge "$expires" ]; then
|
|
292
|
+
if effective_unrestricted "$AGENT_USER"; then echo "state=inconsistent"; echo "effective_authority=true"; echo "status=security-error"; echo "detail=expiry passed but privilege remains";
|
|
293
|
+
else echo "state=expired"; echo "effective_authority=false"; fi
|
|
294
|
+
exit 0
|
|
295
|
+
fi
|
|
296
|
+
if ! effective_unrestricted "$AGENT_USER"; then echo "state=inconsistent"; echo "effective_authority=false"; echo "status=broken/inconsistent"; echo "detail=exact sudo execution is not effective"; exit 0; fi
|
|
297
|
+
if ! service_active agent-temporary-expire; then echo "state=inconsistent"; echo "effective_authority=true"; echo "status=security-error"; echo "detail=expiry timer is not active"; exit 0; fi
|
|
298
|
+
persist=$(state_value persist_reboot); [ "$persist" = yes ] || persist=no
|
|
299
|
+
remaining=$((expires - now)); echo "state=active"; echo "effective_authority=true"; echo "user=$AGENT_USER"; echo "version=$(state_value version)"; echo "activated_at_epoch=$(state_value activated_at_epoch)"; echo "expires_at_epoch=$expires"; echo "remaining_seconds=$remaining"; echo "ttl=$(state_value ttl)"; echo "persist_reboot=$persist"; echo "scope=unrestricted-root"; echo "authentication=NOPASSWD"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if [ "$OS" = Darwin ]; then
|
|
303
|
+
case "${1:-status}" in
|
|
304
|
+
install) shift; INSTALL_USER=; [ "${1:-}" = --user ] && { [ "$#" -eq 2 ] || usage; INSTALL_USER=$2; }; do_install_macos ;;
|
|
305
|
+
version) [ "$#" -eq 1 ] || usage; echo "agent-temporary $VERSION" ;;
|
|
306
|
+
--validate-ttl) [ "$#" -eq 2 ] || usage; parse_ttl "$2" >/dev/null; echo valid ;;
|
|
307
|
+
*) exec /usr/local/libexec/agent-temporary-macos "$@" ;;
|
|
308
|
+
esac
|
|
309
|
+
exit 0
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
case "${1:-status}" in
|
|
313
|
+
on) shift; do_on "$@" ;; off) do_off ;; status)
|
|
314
|
+
shift; STATUS_JSON=no
|
|
315
|
+
if [ "$#" -eq 1 ] && [ "$1" = --json ]; then STATUS_JSON=yes
|
|
316
|
+
elif [ "$#" -ne 0 ]; then usage
|
|
317
|
+
fi
|
|
318
|
+
if [ "$STATUS_JSON" = yes ]; then
|
|
319
|
+
do_status | awk -F= 'BEGIN { printf "{"; first=1 }
|
|
320
|
+
{ key=$1; value=substr($0, index($0,"=")+1); gsub(/\\/,"\\\\",value); gsub(/"/,"\\\"",value); if (!first) printf ","; printf "\"%s\":\"%s\"",key,value; first=0 }
|
|
321
|
+
END { print "}" }'
|
|
322
|
+
else do_status
|
|
323
|
+
fi ;;
|
|
324
|
+
version) version ;;
|
|
325
|
+
--validate-ttl) [ "$#" -eq 2 ] || usage; parse_ttl "$2" >/dev/null; echo valid ;;
|
|
326
|
+
--internal-expire) do_expire ;; --internal-expire-loop) do_expire_loop ;; --internal-boot-revoke) do_boot_revoke ;; *) usage ;;
|
|
327
|
+
esac
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/sbin/openrc-run
|
|
2
|
+
|
|
3
|
+
name="agent-temporary-expire"
|
|
4
|
+
command="/usr/local/bin/agent-temporary"
|
|
5
|
+
command_args="--internal-expire-loop"
|
|
6
|
+
command_background=true
|
|
7
|
+
supervisor="supervise-daemon"
|
|
8
|
+
respawn_delay=30
|
|
9
|
+
respawn_max=5
|
|
10
|
+
respawn_period=600
|
|
11
|
+
|
|
12
|
+
depend() {
|
|
13
|
+
need localmount
|
|
14
|
+
after bootmisc
|
|
15
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
VERSION=0.7.0
|
|
4
|
+
RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-agent-temporary}
|
|
5
|
+
STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/db/agent-temporary}
|
|
6
|
+
STATE=${AGENT_TEMPORARY_STATE:-$STATE_DIR/state}
|
|
7
|
+
LOCK=${AGENT_TEMPORARY_LOCK:-$STATE_DIR/.lock}
|
|
8
|
+
SUDOERS=${AGENT_TEMPORARY_SUDOERS:-/etc/sudoers}
|
|
9
|
+
CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
|
|
10
|
+
EXPIRY_LABEL=com.agent-temporary.expire
|
|
11
|
+
|
|
12
|
+
ID=${ID:-/usr/bin/id}
|
|
13
|
+
DATE=${DATE:-/bin/date}
|
|
14
|
+
SYSCTL=${SYSCTL:-/usr/sbin/sysctl}
|
|
15
|
+
MKTEMP=${MKTEMP:-/usr/bin/mktemp}
|
|
16
|
+
MV=${MV:-/bin/mv}
|
|
17
|
+
RM=${RM:-/bin/rm}
|
|
18
|
+
MKDIR=${MKDIR:-/bin/mkdir}
|
|
19
|
+
RMDIR=${RMDIR:-/bin/rmdir}
|
|
20
|
+
CHMOD=${CHMOD:-/bin/chmod}
|
|
21
|
+
CHOWN=${CHOWN:-/usr/sbin/chown}
|
|
22
|
+
VISUDO=${VISUDO:-/usr/sbin/visudo}
|
|
23
|
+
LAUNCHCTL=${LAUNCHCTL:-/bin/launchctl}
|
|
24
|
+
PROBE=${PROBE:-}
|
|
25
|
+
SUDO=${SUDO:-/usr/bin/sudo}
|
|
26
|
+
SU=${SU:-/usr/bin/su}
|
|
27
|
+
TRUE=${TRUE:-/usr/bin/true}
|
|
28
|
+
KICK_ATTEMPTS=${KICK_ATTEMPTS:-100}
|
|
29
|
+
KICK_SLEEP=${KICK_SLEEP:-0.1}
|
|
30
|
+
REAPER_LOCK_ATTEMPTS=${REAPER_LOCK_ATTEMPTS:-100}
|
|
31
|
+
REAPER_LOCK_SLEEP=${REAPER_LOCK_SLEEP:-0.1}
|
|
32
|
+
|
|
33
|
+
die() { echo "agent-temporary: $*" >&2; exit 1; }
|
|
34
|
+
usage() { echo "usage: agent-temporary-macos on [--ttl MINUTESm|HOURSh] [--persist-reboot] | off | status | --reaper | --boot-revoke" >&2; exit 2; }
|
|
35
|
+
|
|
36
|
+
require_root() { [ "$($ID -u 2>/dev/null)" = 0 ] || die "root is required"; }
|
|
37
|
+
|
|
38
|
+
load_user() {
|
|
39
|
+
TARGET_USER=${AGENT_TEMPORARY_USER:-}
|
|
40
|
+
if [ -r "$CONFIG" ]; then
|
|
41
|
+
CONFIG_USER=
|
|
42
|
+
. "$CONFIG"
|
|
43
|
+
[ -n "$CONFIG_USER" ] && TARGET_USER=$CONFIG_USER
|
|
44
|
+
fi
|
|
45
|
+
[ -n "$TARGET_USER" ] || die "configured target user is required"
|
|
46
|
+
"$ID" "$TARGET_USER" >/dev/null 2>&1 || die "unknown target user: $TARGET_USER"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
now() { "$DATE" +%s; }
|
|
50
|
+
boot_identity() {
|
|
51
|
+
raw=$("$SYSCTL" -n kern.boottime 2>/dev/null) || return 1
|
|
52
|
+
sec=$(printf '%s\n' "$raw" | sed -n 's/.*{[[:space:]]*sec[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')
|
|
53
|
+
usec=$(printf '%s\n' "$raw" | sed -n 's/.*,[[:space:]]*usec[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')
|
|
54
|
+
digits "$sec" && digits "$usec" || return 1
|
|
55
|
+
printf '%s:%s\n' "$sec" "$usec"
|
|
56
|
+
}
|
|
57
|
+
state_value() { [ -f "$STATE" ] || return 1; sed -n "s/^$1=//p" "$STATE" | sed -n '1p'; }
|
|
58
|
+
digits() { case $1 in ''|*[!0-9]*) return 1;; *) return 0;; esac; }
|
|
59
|
+
|
|
60
|
+
parse_ttl() {
|
|
61
|
+
ttl=$1
|
|
62
|
+
case "$ttl" in
|
|
63
|
+
[0-9]*m) number=${ttl%m}; unit=m;;
|
|
64
|
+
[0-9]*h) number=${ttl%h}; unit=h;;
|
|
65
|
+
*) die "TTL must be a positive integer number of minutes or hours";;
|
|
66
|
+
esac
|
|
67
|
+
digits "$number" || die "malformed TTL: $ttl"
|
|
68
|
+
while [ "${number#0}" != "$number" ]; do number=${number#0}; done
|
|
69
|
+
[ -n "$number" ] || die "TTL must be greater than zero"
|
|
70
|
+
case "$unit" in
|
|
71
|
+
m) [ "$number" -le 480 ] || die "TTL maximum is 8h"; seconds=$((number * 60));;
|
|
72
|
+
h) [ "$number" -le 8 ] || die "TTL maximum is 8h"; seconds=$((number * 3600));;
|
|
73
|
+
esac
|
|
74
|
+
[ "$seconds" -ge 300 ] || die "TTL minimum is 5m"
|
|
75
|
+
echo "$seconds"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
valid_state() {
|
|
79
|
+
[ -f "$STATE" ] || return 1
|
|
80
|
+
awk -F= '
|
|
81
|
+
BEGIN { ok=1; count=0 }
|
|
82
|
+
NF != 2 { ok=0; next }
|
|
83
|
+
$1 !~ /^(phase|user|issued_at|expires_at|boot_identity|persist_reboot)$/ { ok=0 }
|
|
84
|
+
{ seen[$1]++; if (seen[$1] > 1) ok=0 }
|
|
85
|
+
{ count++ }
|
|
86
|
+
END {
|
|
87
|
+
if (count != 6) ok=0
|
|
88
|
+
if (!seen["phase"] || !seen["user"] || !seen["issued_at"] || !seen["expires_at"] || !seen["boot_identity"] || !seen["persist_reboot"]) ok=0
|
|
89
|
+
exit(ok ? 0 : 1)
|
|
90
|
+
}
|
|
91
|
+
' "$STATE" 2>/dev/null || return 1
|
|
92
|
+
phase=$(state_value phase) || return 1
|
|
93
|
+
user=$(state_value user) || return 1
|
|
94
|
+
issued=$(state_value issued_at) || return 1
|
|
95
|
+
expires=$(state_value expires_at) || return 1
|
|
96
|
+
boot=$(state_value boot_identity) || return 1
|
|
97
|
+
persist=$(state_value persist_reboot) || return 1
|
|
98
|
+
[ "$phase" = active ] || [ "$phase" = prepared ] || return 1
|
|
99
|
+
[ -n "$user" ] && digits "$issued" && digits "$expires" && [ "$issued" -le "$expires" ] && [ -n "$boot" ] && { [ "$persist" = yes ] || [ "$persist" = no ]; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
secure_state_dir() {
|
|
103
|
+
"$MKDIR" -p "$STATE_DIR" 2>/dev/null || die "cannot create state directory"
|
|
104
|
+
"$CHMOD" 700 "$STATE_DIR" 2>/dev/null || die "cannot secure state directory"
|
|
105
|
+
"$CHOWN" root:wheel "$STATE_DIR" 2>/dev/null || die "cannot secure state ownership"
|
|
106
|
+
}
|
|
107
|
+
acquire_lock() {
|
|
108
|
+
secure_state_dir
|
|
109
|
+
"$MKDIR" "$LOCK" 2>/dev/null || die "transaction already in progress"
|
|
110
|
+
"$CHMOD" 700 "$LOCK" 2>/dev/null || die "cannot secure transaction lock"
|
|
111
|
+
"$CHOWN" root:wheel "$LOCK" 2>/dev/null || die "cannot secure transaction lock ownership"
|
|
112
|
+
trap release_lock EXIT INT TERM
|
|
113
|
+
}
|
|
114
|
+
acquire_lock_wait() {
|
|
115
|
+
secure_state_dir
|
|
116
|
+
attempts=0
|
|
117
|
+
while ! "$MKDIR" "$LOCK" 2>/dev/null; do
|
|
118
|
+
[ "$attempts" -lt "$REAPER_LOCK_ATTEMPTS" ] || die "reaper transaction lock wait expired"
|
|
119
|
+
attempts=$((attempts + 1)); sleep "$REAPER_LOCK_SLEEP"
|
|
120
|
+
done
|
|
121
|
+
"$CHMOD" 700 "$LOCK" 2>/dev/null || die "cannot secure transaction lock"
|
|
122
|
+
"$CHOWN" root:wheel "$LOCK" 2>/dev/null || die "cannot secure transaction lock ownership"
|
|
123
|
+
trap release_lock EXIT INT TERM
|
|
124
|
+
}
|
|
125
|
+
release_lock() { "$RMDIR" "$LOCK" 2>/dev/null || true; trap - EXIT INT TERM; }
|
|
126
|
+
remove_rule() { [ -e "$RULE" ] && "$RM" -f "$RULE"; }
|
|
127
|
+
clear_state() { [ -e "$STATE" ] && "$RM" -f "$STATE"; }
|
|
128
|
+
validate_full() { "$VISUDO" -cf "$SUDOERS" >/dev/null 2>&1; }
|
|
129
|
+
|
|
130
|
+
exact_probe() {
|
|
131
|
+
if [ -n "$PROBE" ]; then "$PROBE" "$1"; else "$SU" "$1" -c "$SUDO -k; $SUDO -n $TRUE"; fi
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
daemon_running() {
|
|
135
|
+
"$LAUNCHCTL" print "system/$EXPIRY_LABEL" 2>/dev/null | grep -q 'state = running'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
kick_reaper() {
|
|
139
|
+
"$LAUNCHCTL" print "system/$EXPIRY_LABEL" >/dev/null 2>&1 || return 1
|
|
140
|
+
"$LAUNCHCTL" kickstart -p "system/$EXPIRY_LABEL" >/dev/null 2>&1 || return 1
|
|
141
|
+
i=0
|
|
142
|
+
while [ "$i" -lt "$KICK_ATTEMPTS" ]; do
|
|
143
|
+
daemon_running && return 0
|
|
144
|
+
i=$((i + 1)); sleep "$KICK_SLEEP"
|
|
145
|
+
done
|
|
146
|
+
return 1
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
write_state() {
|
|
150
|
+
phase=$1 user=$2 issued=$3 expires=$4 boot=$5 persist=$6
|
|
151
|
+
tmp=$($MKTEMP "$STATE.XXXXXX") || return 1
|
|
152
|
+
if ! { printf 'phase=%s\n' "$phase"; printf 'user=%s\n' "$user"; printf 'issued_at=%s\n' "$issued"; printf 'expires_at=%s\n' "$expires"; printf 'boot_identity=%s\n' "$boot"; printf 'persist_reboot=%s\n' "$persist"; } >"$tmp"; then
|
|
153
|
+
"$RM" -f "$tmp"; return 1
|
|
154
|
+
fi
|
|
155
|
+
"$CHMOD" 600 "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
|
|
156
|
+
"$CHOWN" root:wheel "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
|
|
157
|
+
"$MV" "$tmp" "$STATE" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
write_candidate() {
|
|
161
|
+
user=$1
|
|
162
|
+
tmp=$($MKTEMP "$RULE.XXXXXX") || return 1
|
|
163
|
+
if ! { printf 'Defaults:%s !authenticate\n' "$user"; printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$user"; } >"$tmp"; then
|
|
164
|
+
"$RM" -f "$tmp"; return 1
|
|
165
|
+
fi
|
|
166
|
+
"$CHMOD" 440 "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
|
|
167
|
+
"$CHOWN" root:wheel "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
|
|
168
|
+
"$VISUDO" -cf "$tmp" >/dev/null 2>&1 || { "$RM" -f "$tmp"; return 1; }
|
|
169
|
+
CANDIDATE=$tmp
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
rollback() {
|
|
173
|
+
[ -n "${CANDIDATE:-}" ] && "$RM" -f "$CANDIDATE" 2>/dev/null || true
|
|
174
|
+
remove_rule; clear_state; validate_full || true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
revoke_locked() {
|
|
178
|
+
user=${1:-}; validation_failed=0
|
|
179
|
+
remove_rule
|
|
180
|
+
validate_full || validation_failed=1
|
|
181
|
+
clear_state
|
|
182
|
+
if [ -n "$user" ] && exact_probe "$user" >/dev/null 2>&1; then probe_failed=1; else probe_failed=0; fi
|
|
183
|
+
[ "$validation_failed" = 0 ] && [ "$probe_failed" = 0 ]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
classify_before_on() {
|
|
187
|
+
if [ -e "$RULE" ] && ! valid_state; then revoke_locked "${TARGET_USER:-}" || true; return 0; fi
|
|
188
|
+
if [ -f "$STATE" ] && [ ! -e "$RULE" ]; then clear_state; return 0; fi
|
|
189
|
+
if valid_state; then
|
|
190
|
+
phase=$(state_value phase); expires=$(state_value expires_at); existing_persist=$(state_value persist_reboot)
|
|
191
|
+
if [ "$phase" != active ] || [ "$expires" -le "$(now)" ]; then
|
|
192
|
+
revoke_locked "$(state_value user)" || true
|
|
193
|
+
elif [ -e "$RULE" ]; then
|
|
194
|
+
if exact_probe "$(state_value user)" >/dev/null 2>&1 && daemon_running; then
|
|
195
|
+
[ "$existing_persist" = "$REQUESTED_PERSIST" ] || die "already active; run off before changing persistence"
|
|
196
|
+
echo "already active; expires_at=$expires; no changes made."; exit 0
|
|
197
|
+
fi
|
|
198
|
+
revoke_locked "$(state_value user)" || true
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
on_cmd() {
|
|
204
|
+
require_root; load_user; ttl=300; REQUESTED_PERSIST=no
|
|
205
|
+
while [ "$#" -gt 0 ]; do
|
|
206
|
+
case $1 in
|
|
207
|
+
--ttl) [ "$#" -ge 2 ] || usage; ttl=$(parse_ttl "$2") || exit 1; shift 2;;
|
|
208
|
+
--persist-reboot) [ "$REQUESTED_PERSIST" = no ] || usage; REQUESTED_PERSIST=yes; shift;;
|
|
209
|
+
*) usage;;
|
|
210
|
+
esac
|
|
211
|
+
done
|
|
212
|
+
acquire_lock; classify_before_on
|
|
213
|
+
started=$(now); expires=$((started + ttl)); boot=$(boot_identity) || { release_lock; die "cannot determine boot identity"; }
|
|
214
|
+
write_candidate "$TARGET_USER" || { rollback; release_lock; die "cannot prepare sudoers candidate"; }
|
|
215
|
+
write_state prepared "$TARGET_USER" "$started" "$expires" "$boot" "$REQUESTED_PERSIST" || { rollback; release_lock; die "cannot write prepared state"; }
|
|
216
|
+
"$MV" "$CANDIDATE" "$RULE" 2>/dev/null || { rollback; release_lock; die "cannot install sudoers fragment"; }
|
|
217
|
+
CANDIDATE=
|
|
218
|
+
validate_full || { rollback; release_lock; die "complete sudoers validation failed"; }
|
|
219
|
+
exact_probe "$TARGET_USER" >/dev/null 2>&1 || { rollback; release_lock; die "exact authority probe failed"; }
|
|
220
|
+
write_state active "$TARGET_USER" "$started" "$expires" "$boot" "$REQUESTED_PERSIST" || { rollback; release_lock; die "cannot commit active state"; }
|
|
221
|
+
kick_reaper || { rollback; release_lock; die "expiry launchd job is not ready"; }
|
|
222
|
+
echo "Temporary agent access enabled for $TARGET_USER until epoch $expires."
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
status_cmd() {
|
|
226
|
+
if [ "$($ID -u 2>/dev/null)" -ne 0 ]; then
|
|
227
|
+
if [ ! -e "$RULE" ] && [ ! -e "$STATE" ]; then
|
|
228
|
+
echo 'state=inactive'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0
|
|
229
|
+
fi
|
|
230
|
+
echo 'status=inspection-requires-privilege'
|
|
231
|
+
echo 'detail=root privileges are required to inspect effective temporary sudo state'
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
acquire_lock
|
|
235
|
+
if [ ! -e "$RULE" ] && [ ! -e "$STATE" ]; then echo 'state=inactive'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
|
|
236
|
+
if ! valid_state; then remove_rule; clear_state; validate_full || true; echo 'state=inconsistent'; echo 'detail=orphan-or-invalid-authority'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
|
|
237
|
+
user=$(state_value user); expires=$(state_value expires_at)
|
|
238
|
+
if [ ! -e "$RULE" ]; then clear_state; echo 'state=inconsistent'; echo 'detail=state-without-authority'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
|
|
239
|
+
if [ "$expires" -le "$(now)" ]; then revoke_locked "$user" || true; echo 'state=expired'; echo 'effective_authority=false'; echo "expires_at=$expires"; echo "version=$VERSION"; return 0; fi
|
|
240
|
+
if ! daemon_running; then revoke_locked "$user" || true; echo 'state=inconsistent'; echo 'detail=reaper-unavailable'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
|
|
241
|
+
if ! exact_probe "$user" >/dev/null 2>&1; then revoke_locked "$user" || true; echo 'state=inconsistent'; echo 'detail=effective-authority-failed'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
|
|
242
|
+
echo 'state=active'; echo 'effective_authority=true'; echo "user=$user"; echo "issued_at=$(state_value issued_at)"; echo "expires_at=$expires"; echo "remaining_seconds=$((expires - $(now)))"; echo "persist_reboot=$(state_value persist_reboot)"; echo "version=$VERSION"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
off_cmd() {
|
|
246
|
+
require_root; user=${AGENT_TEMPORARY_USER:-}; [ -f "$STATE" ] && user=$(state_value user 2>/dev/null || printf '%s' "$user"); acquire_lock_wait
|
|
247
|
+
revoke_locked "$user" || { release_lock; die "revocation validation or exact probe failed"; }
|
|
248
|
+
echo 'Temporary agent access disabled.'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
reaper_cmd() {
|
|
252
|
+
require_root; acquire_lock_wait
|
|
253
|
+
if ! valid_state || [ ! -e "$RULE" ]; then revoke_locked "$(state_value user 2>/dev/null || true)" || true; release_lock; exit 0; fi
|
|
254
|
+
user=$(state_value user); expires=$(state_value expires_at); boot=$(state_value boot_identity); current_boot=$(boot_identity)
|
|
255
|
+
if [ "$expires" -le "$(now)" ] || [ "$boot" != "$current_boot" ]; then revoke_locked "$user" || true; release_lock; exit 0; fi
|
|
256
|
+
release_lock
|
|
257
|
+
while :; do
|
|
258
|
+
[ -f "$STATE" ] && [ -e "$RULE" ] || exit 0
|
|
259
|
+
[ "$(now)" -ge "$expires" ] && { acquire_lock; revoke_locked "$user" || true; release_lock; exit 0; }
|
|
260
|
+
sleep 1
|
|
261
|
+
done
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
boot_reconcile_cmd() {
|
|
265
|
+
require_root; acquire_lock
|
|
266
|
+
if [ ! -f "$STATE" ]; then remove_rule; validate_full || true; return 0; fi
|
|
267
|
+
if ! valid_state; then revoke_locked "$(state_value user 2>/dev/null || true)" || true; return 1; fi
|
|
268
|
+
phase=$(state_value phase); user=$(state_value user); issued=$(state_value issued_at); expires=$(state_value expires_at); persist=$(state_value persist_reboot); current_boot=$(boot_identity)
|
|
269
|
+
if [ "$phase" != active ] || [ "$persist" != yes ] || [ "$expires" -le "$(now)" ] || [ "$(now)" -lt "$issued" ]; then
|
|
270
|
+
revoke_locked "$user" || true; return 0
|
|
271
|
+
fi
|
|
272
|
+
if [ -z "$current_boot" ]; then revoke_locked "$user" || true; return 1; fi
|
|
273
|
+
CANDIDATE=
|
|
274
|
+
write_candidate "$user" || { rollback; return 1; }
|
|
275
|
+
"$MV" "$CANDIDATE" "$RULE" 2>/dev/null || { rollback; return 1; }
|
|
276
|
+
CANDIDATE=
|
|
277
|
+
validate_full || { rollback; return 1; }
|
|
278
|
+
exact_probe "$user" >/dev/null 2>&1 || { rollback; return 1; }
|
|
279
|
+
write_state active "$user" "$issued" "$expires" "$current_boot" yes || { rollback; return 1; }
|
|
280
|
+
kick_reaper || { rollback; return 1; }
|
|
281
|
+
return 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case ${1:-} in
|
|
285
|
+
on) shift; on_cmd "$@";;
|
|
286
|
+
off) shift; [ "$#" -eq 0 ] || usage; off_cmd;;
|
|
287
|
+
status) shift; [ "$#" -eq 0 ] || usage; status_cmd;;
|
|
288
|
+
--reaper) [ "$#" -eq 1 ] || usage; reaper_cmd;;
|
|
289
|
+
--boot-revoke) [ "$#" -eq 1 ] || usage; boot_reconcile_cmd;;
|
|
290
|
+
version|--version) [ "$#" -eq 1 ] || usage; echo "agent-temporary $VERSION";;
|
|
291
|
+
*) usage;;
|
|
292
|
+
esac
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.agent-temporary.boot</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/usr/local/libexec/agent-temporary-macos</string>
|
|
10
|
+
<string>--boot-revoke</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>RunAtLoad</key>
|
|
13
|
+
<true/>
|
|
14
|
+
<key>KeepAlive</key>
|
|
15
|
+
<dict>
|
|
16
|
+
<key>SuccessfulExit</key>
|
|
17
|
+
<false/>
|
|
18
|
+
</dict>
|
|
19
|
+
<key>ThrottleInterval</key>
|
|
20
|
+
<integer>60</integer>
|
|
21
|
+
</dict>
|
|
22
|
+
</plist>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.agent-temporary.expire</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/usr/local/libexec/agent-temporary-reaper</string>
|
|
10
|
+
<string>--reaper</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>ThrottleInterval</key>
|
|
13
|
+
<integer>60</integer>
|
|
14
|
+
</dict>
|
|
15
|
+
</plist>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
PREFIX=${PREFIX:-/usr/local/bin}
|
|
5
|
+
TARGET=${TARGET:-$PREFIX/agent-temporary}
|
|
6
|
+
UNAME=${UNAME:-uname}
|
|
7
|
+
OS=$($UNAME -s 2>/dev/null || true)
|
|
8
|
+
if [ "$OS" = Darwin ]; then
|
|
9
|
+
RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-agent-temporary}
|
|
10
|
+
LEGACY_RULE=${AGENT_TEMPORARY_LEGACY_RULE:-/etc/sudoers.d/90-temporary-agent}
|
|
11
|
+
STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/db/agent-temporary}
|
|
12
|
+
[ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0" >&2; exit 1; }
|
|
13
|
+
[ ! -e "$LEGACY_RULE" ] || { echo "Security error: legacy sudoers authority fragment exists at $LEGACY_RULE; inspect and intentionally clean it up before uninstall" >&2; exit 1; }
|
|
14
|
+
[ -x /usr/local/libexec/agent-temporary-macos ] || { [ ! -e "$RULE" ] || { echo "macOS backend is missing; refusing unsafe uninstall" >&2; exit 1; }; }
|
|
15
|
+
if [ -e "$RULE" ] || [ -e "$STATE_DIR/state" ]; then
|
|
16
|
+
/usr/local/libexec/agent-temporary-macos off || { echo "Could not revoke managed macOS authority" >&2; exit 1; }
|
|
17
|
+
fi
|
|
18
|
+
/usr/sbin/visudo -cf /etc/sudoers >/dev/null || { echo "sudoers validation failed after revoke" >&2; exit 1; }
|
|
19
|
+
/bin/launchctl bootout system/com.agent-temporary.expire >/dev/null 2>&1 || true
|
|
20
|
+
/bin/launchctl bootout system/com.agent-temporary.boot >/dev/null 2>&1 || true
|
|
21
|
+
rm -f /Library/LaunchDaemons/com.agent-temporary.expire.plist /Library/LaunchDaemons/com.agent-temporary.boot.plist /usr/local/libexec/agent-temporary-macos /usr/local/libexec/agent-temporary-reaper "$TARGET" /etc/agent-temporary.conf
|
|
22
|
+
rm -rf "$STATE_DIR"
|
|
23
|
+
/usr/sbin/visudo -cf /etc/sudoers >/dev/null || { echo "sudoers validation failed after uninstall" >&2; exit 1; }
|
|
24
|
+
echo "Removed macOS agent-temporary runtime, privilege rule, launchd jobs, and state."
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-temporary-agent}
|
|
28
|
+
CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
|
|
29
|
+
STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/lib/agent-temporary}
|
|
30
|
+
SYSTEMD_DIR=${AGENT_TEMPORARY_SYSTEMD_DIR:-/etc/systemd/system}
|
|
31
|
+
OPENRC_DIR=${AGENT_TEMPORARY_OPENRC_DIR:-/etc/init.d}
|
|
32
|
+
SYSTEMCTL=${SYSTEMCTL:-systemctl}
|
|
33
|
+
RCSERVICE=${RCSERVICE:-/sbin/rc-service}
|
|
34
|
+
RCUPDATE=${RCUPDATE:-/sbin/rc-update}
|
|
35
|
+
|
|
36
|
+
[ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0" >&2; exit 1; }
|
|
37
|
+
rm -f "$RULE"
|
|
38
|
+
if command -v "$SYSTEMCTL" >/dev/null 2>&1 && { [ -d /run/systemd/system ] || "$SYSTEMCTL" show-environment >/dev/null 2>&1; }; then
|
|
39
|
+
"$SYSTEMCTL" disable --now agent-temporary-expire.timer >/dev/null 2>&1 || true
|
|
40
|
+
rm -f "$SYSTEMD_DIR/agent-temporary-expire.service" "$SYSTEMD_DIR/agent-temporary-expire.timer" "$SYSTEMD_DIR/agent-temporary-boot.service"
|
|
41
|
+
"$SYSTEMCTL" daemon-reload >/dev/null 2>&1 || true
|
|
42
|
+
elif [ -x "$RCSERVICE" ]; then
|
|
43
|
+
"$RCSERVICE" agent-temporary-expire stop >/dev/null 2>&1 || true
|
|
44
|
+
"$RCUPDATE" del agent-temporary-boot default >/dev/null 2>&1 || true
|
|
45
|
+
"$RCUPDATE" del agent-temporary-expire default >/dev/null 2>&1 || true
|
|
46
|
+
"$RCSERVICE" agent-temporary-boot stop >/dev/null 2>&1 || true
|
|
47
|
+
rm -f "$OPENRC_DIR/agent-temporary-boot" "$OPENRC_DIR/agent-temporary-expire"
|
|
48
|
+
fi
|
|
49
|
+
rm -f "$CONFIG" "$TARGET"
|
|
50
|
+
rm -rf "$STATE_DIR"
|
|
51
|
+
if command -v visudo >/dev/null 2>&1; then visudo -cf /etc/sudoers >/dev/null; fi
|
|
52
|
+
echo "Removed agent-temporary runtime, privilege rule, service integration, and state."
|