sshifu 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "sshifu",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "SSH authentication system with short-lived certificates and OAuth authentication (GitHub organizations)",
5
5
  "main": "bin/sshifu.js",
6
6
  "bin": {
7
- "sshifu": "bin/sshifu.js",
8
- "sshifu-server": "bin/sshifu-server.js",
9
- "sshifu-trust": "bin/sshifu-trust.js"
7
+ "sshifu": "bin/sshifu.js"
10
8
  },
11
9
  "scripts": {
12
10
  "postinstall": "node scripts/install.js"
package/README.md DELETED
@@ -1,254 +0,0 @@
1
- # Sshifu
2
-
3
- <div align="center">
4
- <img src="logo.png" alt="Sshifu Logo" width="250px">
5
- </div>
6
-
7
- **Sshifu** (SSH + Fu / 師傅 "master") is a lightweight SSH authentication system that uses short-lived OpenSSH certificates with OAuth authentication (GitHub organizations).
8
-
9
- A minimal alternative to complex SSH access platforms like Teleport, while remaining fully compatible with existing OpenSSH tooling.
10
-
11
- ## Features
12
-
13
- - 🔐 **Short-lived SSH certificates** - Automatic certificate issuance with configurable TTL (default 8 hours)
14
- - 🌐 **GitHub OAuth authentication** - Authenticate users via GitHub organization membership
15
- - 🛠️ **Standard OpenSSH compatibility** - Works with existing `ssh` command without workflow changes
16
- - 📦 **Minimal infrastructure** - Single server component, no database required
17
- - 👥 **Designed for small teams** - Optimized for teams with <50 users
18
-
19
- ## Quick Start
20
-
21
- ```bash
22
- # Expose your server publicly (here we're using localhost.run for testing)
23
- ssh -R 80:localhost:8080 nokey@localhost.run
24
-
25
- # Prepare GitHub OAuth credentials & note the localhost.run URL from above.
26
- # In another terminal, start the sshifu server and follow the wizard.
27
- npx sshifu-server
28
-
29
- # On the SSH server machine, configure trust (requires sudo)
30
- sudo npx sshifu-trust <localhost.run address>
31
-
32
- # From another machine, connect to the SSH server
33
- npx sshifu <localhost.run address> <ssh server address>
34
- ```
35
-
36
- ## Architecture
37
-
38
- ```
39
- ┌─────────────┐
40
- │ User CLI │ sshifu
41
- │ (sshifu) │
42
- └──────┬──────┘
43
-
44
- │ 1. Start login session
45
- │ 2. Poll for approval
46
- │ 3. Request certificate
47
-
48
- ┌─────────────────────────┐
49
- │ sshifu-server │
50
- │ - OAuth gateway │
51
- │ - SSH Certificate Auth │
52
- └─────────────────────────┘
53
-
54
- │ Configure trust
55
-
56
- ┌─────────────────────────┐
57
- │ Target SSH Server │
58
- │ (configured via │
59
- │ sshifu-trust) │
60
- └─────────────────────────┘
61
- ```
62
-
63
- ## Components
64
-
65
- | Tool | Purpose |
66
- |------|---------|
67
- | `sshifu` | CLI used by users to authenticate and connect to SSH servers |
68
- | `sshifu-server` | Web server acting as OAuth gateway and SSH Certificate Authority |
69
- | `sshifu-trust` | Server-side CLI to configure SSH servers to trust the Sshifu CA |
70
-
71
- ## Installation Options
72
-
73
- #### Option 1: Install Globally via npm
74
-
75
- For frequent use, install globally:
76
-
77
- ```bash
78
- npm install -g sshifu
79
- ```
80
-
81
- Then use commands directly:
82
-
83
- ```bash
84
- sshifu auth.example.com user@target-server.com
85
- sshifu-server
86
- sshifu-trust auth.example.com
87
- ```
88
-
89
- #### Option 2: Pre-built Binary
90
-
91
- Download the latest release for your platform from the [releases page](https://github.com/azophy/sshifu/releases):
92
-
93
- ```bash
94
- # Linux (amd64)
95
- curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-linux-amd64.tar.gz | tar xz
96
- sudo mv sshifu* /usr/local/bin/
97
-
98
- # macOS (Intel)
99
- curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-darwin-amd64.tar.gz | tar xz
100
- sudo mv sshifu* /usr/local/bin/
101
-
102
- # macOS (Apple Silicon)
103
- curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-darwin-arm64.tar.gz | tar xz
104
- sudo mv sshifu* /usr/local/bin/
105
-
106
- # Windows (amd64)
107
- curl -L https://github.com/azophy/sshifu/releases/latest/download/sshifu-windows-amd64.zip -o sshifu.zip
108
- unzip sshifu.zip
109
- # Move sshifu.exe to a directory in your PATH
110
- ```
111
-
112
- #### Option 3: Build from Source
113
-
114
- Requires Go 1.25+:
115
-
116
- ```bash
117
- go build ./cmd/sshifu
118
- go build ./cmd/sshifu-server
119
- go build ./cmd/sshifu-trust
120
- ```
121
-
122
- ## Getting GitHub OAuth Client ID & Secret
123
-
124
- To configure OAuth authentication with GitHub, you need to create a GitHub OAuth App:
125
-
126
- 1. Go to your GitHub organization's settings: `https://github.com/organizations/<your-org>/settings`
127
- 2. Navigate to **Developer settings** (left sidebar)
128
- 3. Click **OAuth Apps** → **New OAuth App**
129
- 4. Fill in the application details:
130
- - **Application name**: `sshifu` (or any descriptive name)
131
- - **Homepage URL**: Your sshifu-server public URL (e.g., `https://auth.example.com`)
132
- - **Authorization callback URL**: Same as your sshifu-server public URL (e.g., `https://auth.example.com`)
133
- 5. Click **Register application**
134
- 6. After registration, you'll see your **Client ID** - copy it
135
- 7. Click **Generate a new client secret** and copy the secret
136
-
137
- > ⚠️ **Important**: The client secret is only shown once. Store it securely and never commit it to version control.
138
-
139
- ## Authentication Flow
140
-
141
- ```
142
- ┌──────────┐ ┌───────────────┐ ┌──────────┐
143
- │ sshifu │ │ sshifu-server │ │ GitHub │
144
- └────┬─────┘ └───────┬───────┘ └────┬─────┘
145
- │ │ │
146
- │ POST /api/v1/login/start │ │
147
- │─────────────────────────────────>│ │
148
- │ │ │
149
- │ session_id, login_url │ │
150
- │<─────────────────────────────────│ │
151
- │ │ │
152
- │ [Display login URL to user] │ │
153
- │ │ │
154
- │ │ [User opens URL in browser] │
155
- │ │ │
156
- │ │ Redirect to GitHub OAuth │
157
- │ │─────────────────────────────────>│
158
- │ │ │
159
- │ │ User authenticates │
160
- │ │<─────────────────────────────────│
161
- │ │ │
162
- │ │ Verify org membership │
163
- │ │─────────────────────────────────>│
164
- │ │ │
165
- │ GET /api/v1/login/status │ Session approved │
166
- │─────────────────────────────────>│ │
167
- │ │ │
168
- │ status: approved, access_token │ │
169
- │<─────────────────────────────────│ │
170
- │ │ │
171
- │ POST /api/v1/sign/user │ │
172
- │─────────────────────────────────>│ │
173
- │ │ │
174
- │ SSH certificate │ │
175
- │<─────────────────────────────────│ │
176
- │ │ │
177
- │ ssh -o CertificateFile=<cert> │ │
178
- │─────────────────────────────────>│ │
179
- │ [SSH connection established] │
180
- ```
181
-
182
- ## Configuration
183
-
184
- ### Server Configuration
185
-
186
- | Field | Description | Default |
187
- |-------|-------------|---------|
188
- | `server.listen` | Address to listen on | `:8080` |
189
- | `server.public_url` | Public URL of the server | Required |
190
- | `ca.private_key` | Path to CA private key | `./ca` |
191
- | `ca.public_key` | Path to CA public key | `./ca.pub` |
192
- | `cert.ttl` | Certificate time-to-live | `8h` |
193
- | `auth.providers` | OAuth provider configurations | Required |
194
-
195
- ### Certificate Extensions
196
-
197
- By default, issued certificates include:
198
- - `permit-pty` - Allow pseudo-terminal allocation
199
- - `permit-port-forwarding` - Allow TCP port forwarding
200
- - `permit-agent-forwarding` - Allow SSH agent forwarding
201
- - `permit-x11-forwarding` - Allow X11 forwarding
202
-
203
- ## Project Structure
204
-
205
- ```
206
- sshifu/
207
- ├── cmd/
208
- │ ├── sshifu/ # User CLI
209
- │ ├── sshifu-server/ # Server component
210
- │ └── sshifu-trust/ # Server setup tool
211
- ├── internal/
212
- │ ├── api/ # HTTP API handlers
213
- │ ├── cert/ # SSH certificate operations
214
- │ ├── config/ # Configuration loading
215
- │ ├── oauth/ # OAuth provider implementations
216
- │ ├── session/ # Login session management
217
- │ └── ssh/ # SSH utilities
218
- ├── web/ # Web frontend (login pages)
219
- ├── config.example.yml # Example configuration
220
- └── go.mod
221
- ```
222
-
223
- ## Security Considerations
224
-
225
- - **Short-lived certificates** reduce the impact of compromised keys
226
- - **CA private key** should be stored securely on the server
227
- - **GitHub organization membership** is verified on each login
228
- - **No long-term secrets** stored on client machines
229
- - **Transparent authorization** - the target server's OS determines final access permissions
230
-
231
- ## Limitations (v1)
232
-
233
- The following features are intentionally out of scope for the initial release:
234
-
235
- - Role-based access control (RBAC)
236
- - Server access policies
237
- - Automatic account provisioning on SSH servers
238
- - Session recording or audit logging
239
- - Admin dashboard
240
- - Certificate revocation
241
-
242
- ## Requirements
243
-
244
- - OpenSSH 6.7+ (for certificate support)
245
- - Linux/Unix-like operating system (server components)
246
- - Windows, macOS, or Linux (client CLI)
247
-
248
- ## License
249
-
250
- [MIT License](LICENSE)
251
-
252
- ## Contributing
253
-
254
- Contributions are welcome! Please read the contributing guidelines before submitting pull requests.
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * sshifu-server CLI wrapper
5
- * Executes the sshifu-server binary from the bin directory
6
- */
7
-
8
- const { spawn } = require('child_process');
9
- const path = require('path');
10
- const fs = require('fs');
11
-
12
- const binName = process.platform === 'win32' ? 'sshifu-server.exe' : 'sshifu-server';
13
- const binPath = path.join(__dirname, binName);
14
-
15
- if (!fs.existsSync(binPath)) {
16
- console.error(`Error: sshifu-server binary not found at ${binPath}`);
17
- console.error('The postinstall script may have failed. Try running:');
18
- console.error(' npm rebuild sshifu');
19
- process.exit(1);
20
- }
21
-
22
- const child = spawn(binPath, process.argv.slice(2), {
23
- stdio: 'inherit',
24
- });
25
-
26
- child.on('error', (err) => {
27
- console.error(`Failed to start sshifu-server: ${err.message}`);
28
- process.exit(1);
29
- });
30
-
31
- child.on('close', (code) => {
32
- process.exit(code);
33
- });
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * sshifu-trust CLI wrapper
5
- * Executes the sshifu-trust binary from the bin directory
6
- */
7
-
8
- const { spawn } = require('child_process');
9
- const path = require('path');
10
- const fs = require('fs');
11
-
12
- const binName = process.platform === 'win32' ? 'sshifu-trust.exe' : 'sshifu-trust';
13
- const binPath = path.join(__dirname, binName);
14
-
15
- if (!fs.existsSync(binPath)) {
16
- console.error(`Error: sshifu-trust binary not found at ${binPath}`);
17
- console.error('The postinstall script may have failed. Try running:');
18
- console.error(' npm rebuild sshifu');
19
- process.exit(1);
20
- }
21
-
22
- const child = spawn(binPath, process.argv.slice(2), {
23
- stdio: 'inherit',
24
- });
25
-
26
- child.on('error', (err) => {
27
- console.error(`Failed to start sshifu-trust: ${err.message}`);
28
- process.exit(1);
29
- });
30
-
31
- child.on('close', (code) => {
32
- process.exit(code);
33
- });
@@ -1,272 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Postinstall script for sshifu
5
- * Downloads the appropriate binary for the current platform from GitHub Releases
6
- */
7
-
8
- const https = require('https');
9
- const fs = require('fs');
10
- const path = require('path');
11
- const os = require('os');
12
- const zlib = require('zlib');
13
- const { execSync } = require('child_process');
14
-
15
- const PACKAGE_NAME = 'sshifu';
16
- const BINARIES = ['sshifu', 'sshifu-server', 'sshifu-trust'];
17
- const DOWNLOAD_TIMEOUT = 60000; // 60 seconds
18
-
19
- // Map npm platform/arch to Go GOOS/GOARCH
20
- const PLATFORM_MAP = {
21
- 'linux-x64': { os: 'linux', arch: 'amd64' },
22
- 'linux-arm64': { os: 'linux', arch: 'arm64' },
23
- 'linux-arm': { os: 'linux', arch: 'arm' },
24
- 'darwin-x64': { os: 'darwin', arch: 'amd64' },
25
- 'darwin-arm64': { os: 'darwin', arch: 'arm64' },
26
- 'win32-x64': { os: 'windows', arch: 'amd64' },
27
- };
28
-
29
- function getPlatformInfo() {
30
- const npmPlatform = `${process.platform}-${process.arch}`;
31
- const mapped = PLATFORM_MAP[npmPlatform];
32
-
33
- if (!mapped) {
34
- throw new Error(
35
- `Unsupported platform: ${npmPlatform}\n` +
36
- `Supported platforms: ${Object.keys(PLATFORM_MAP).join(', ')}`
37
- );
38
- }
39
-
40
- return mapped;
41
- }
42
-
43
- function getBinaryExtension() {
44
- return process.platform === 'win32' ? '.exe' : '';
45
- }
46
-
47
- function getArchiveExtension() {
48
- return process.platform === 'win32' ? '.zip' : '.tar.gz';
49
- }
50
-
51
- function getDownloadUrl(version, platformInfo) {
52
- const ext = getArchiveExtension();
53
- const archiveName = `${PACKAGE_NAME}-${platformInfo.os}-${platformInfo.arch}${ext}`;
54
- return `https://github.com/azophy/sshifu/releases/download/${version}/${archiveName}`;
55
- }
56
-
57
- function getLatestVersion() {
58
- return new Promise((resolve, reject) => {
59
- const options = {
60
- hostname: 'api.github.com',
61
- path: '/repos/azophy/sshifu/releases/latest',
62
- method: 'GET',
63
- headers: {
64
- 'User-Agent': 'sshifu-npm-installer',
65
- 'Accept': 'application/vnd.github.v3+json',
66
- },
67
- };
68
-
69
- const req = https.get(options, (res) => {
70
- let data = '';
71
-
72
- res.on('data', (chunk) => {
73
- data += chunk;
74
- });
75
-
76
- res.on('end', () => {
77
- if (res.statusCode === 200) {
78
- try {
79
- const parsed = JSON.parse(data);
80
- resolve(parsed.tag_name || parsed.name);
81
- } catch (e) {
82
- reject(new Error('Failed to parse GitHub API response'));
83
- }
84
- } else {
85
- reject(new Error(`GitHub API returned status ${res.statusCode}`));
86
- }
87
- });
88
- });
89
-
90
- req.on('error', reject);
91
- req.on('timeout', () => {
92
- req.destroy();
93
- reject(new Error('Request timed out'));
94
- });
95
- });
96
- }
97
-
98
- function downloadFile(url, totalBytes = 0) {
99
- return new Promise((resolve, reject) => {
100
- const options = {
101
- headers: {
102
- 'User-Agent': 'sshifu-npm-installer',
103
- },
104
- timeout: DOWNLOAD_TIMEOUT,
105
- };
106
-
107
- https.get(url, options, (res) => {
108
- if (res.statusCode === 301 || res.statusCode === 302) {
109
- // Follow redirect
110
- downloadFile(res.headers.location, totalBytes).then(resolve).catch(reject);
111
- return;
112
- }
113
-
114
- if (res.statusCode !== 200) {
115
- reject(new Error(`Download failed with status ${res.statusCode}`));
116
- return;
117
- }
118
-
119
- const chunks = [];
120
- let downloaded = 0;
121
-
122
- res.on('data', (chunk) => {
123
- downloaded += chunk.length;
124
- chunks.push(chunk);
125
- });
126
-
127
- res.on('end', () => {
128
- resolve(Buffer.concat(chunks));
129
- });
130
- }).on('error', reject)
131
- .on('timeout', () => {
132
- reject(new Error('Download timed out'));
133
- });
134
- });
135
- }
136
-
137
- function extractArchive(archiveData, destDir) {
138
- const ext = getArchiveExtension();
139
-
140
- if (ext === '.tar.gz') {
141
- return new Promise((resolve, reject) => {
142
- const tar = require('child_process').spawn('tar', ['xzf', '-', '-C', destDir], {
143
- stdio: ['pipe', 'inherit', 'inherit'],
144
- });
145
-
146
- tar.on('close', (code) => {
147
- if (code === 0) resolve();
148
- else reject(new Error(`tar exited with code ${code}`));
149
- });
150
-
151
- tar.stdin.write(archiveData);
152
- tar.stdin.end();
153
- });
154
- } else if (ext === '.zip') {
155
- // For Windows, write to temp file and extract
156
- const tempFile = path.join(destDir, 'temp.zip');
157
- fs.writeFileSync(tempFile, archiveData);
158
-
159
- try {
160
- // Try PowerShell first
161
- execSync(`powershell -command "Expand-Archive -Path '${tempFile}' -DestinationPath '${destDir}' -Force"`, {
162
- stdio: 'inherit',
163
- });
164
- fs.unlinkSync(tempFile);
165
- return Promise.resolve();
166
- } catch (e) {
167
- // Fallback to other methods if needed
168
- fs.unlinkSync(tempFile);
169
- return Promise.reject(new Error('Failed to extract zip archive'));
170
- }
171
- }
172
-
173
- return Promise.reject(new Error(`Unsupported archive format: ${ext}`));
174
- }
175
-
176
- function makeExecutable(filePath) {
177
- if (process.platform !== 'win32') {
178
- fs.chmodSync(filePath, 0o755);
179
- }
180
- }
181
-
182
- async function install() {
183
- console.log(`🔐 ${PACKAGE_NAME} installer`);
184
- console.log('========================\n');
185
-
186
- const platformInfo = getPlatformInfo();
187
- const binExt = getBinaryExtension();
188
-
189
- console.log(`Platform: ${process.platform}-${process.arch}`);
190
- console.log(`Mapped to: ${platformInfo.os}-${platformInfo.arch}\n`);
191
-
192
- // Determine version
193
- let version;
194
- try {
195
- version = process.env.SSHIFU_VERSION || await getLatestVersion();
196
- } catch (e) {
197
- console.error(`Failed to get version: ${e.message}`);
198
- console.error('\nFalling back to latest release...');
199
- version = 'v0.2.2';
200
- }
201
- console.log(`Version: ${version}`);
202
-
203
- // Get download URL
204
- const downloadUrl = getDownloadUrl(version, platformInfo);
205
- console.log(`Downloading from: ${downloadUrl}\n`);
206
-
207
- // Download archive
208
- console.log('Downloading archive...');
209
- let archiveData;
210
- try {
211
- archiveData = await downloadFile(downloadUrl);
212
- console.log(`Downloaded ${Math.round(archiveData.length / 1024)} KB`);
213
- } catch (e) {
214
- console.error(`Failed to download: ${e.message}`);
215
- console.error('\nMake sure you have a stable internet connection.');
216
- console.error('If the problem persists, download manually from:');
217
- console.error('https://github.com/azophy/sshifu/releases\n');
218
- console.error('Then extract to: ' + path.join(__dirname, '..', 'bin'));
219
- process.exit(1);
220
- }
221
-
222
- // Create bin directory
223
- const binDir = path.join(__dirname, '..', 'bin');
224
- if (!fs.existsSync(binDir)) {
225
- fs.mkdirSync(binDir, { recursive: true });
226
- }
227
-
228
- // Create temp directory for extraction
229
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${PACKAGE_NAME}-`));
230
-
231
- try {
232
- // Extract archive
233
- console.log('Extracting binaries...');
234
- await extractArchive(archiveData, tempDir);
235
-
236
- // Move binaries to bin directory
237
- console.log('Installing binaries...');
238
- for (const binary of BINARIES) {
239
- const srcName = binary + binExt;
240
- const srcPath = path.join(tempDir, srcName);
241
- const destPath = path.join(binDir, srcName);
242
-
243
- if (fs.existsSync(srcPath)) {
244
- fs.copyFileSync(srcPath, destPath);
245
- makeExecutable(destPath);
246
- console.log(` ✓ ${binary}${binExt}`);
247
- } else {
248
- console.warn(` ⚠ ${binary}${binExt} not found in archive`);
249
- }
250
- }
251
-
252
- console.log('\n✅ Installation complete!');
253
- console.log(`\nYou can now run:`);
254
- console.log(` ${BINARIES.map(b => b + binExt).join(', ')}`);
255
- console.log(`\nOr use npx:`);
256
- console.log(` npx sshifu <server> <target>`);
257
-
258
- } catch (e) {
259
- console.error(`Installation failed: ${e.message}`);
260
- console.error('\nStack:', e.stack);
261
- process.exit(1);
262
- } finally {
263
- // Cleanup temp directory
264
- try {
265
- fs.rmSync(tempDir, { recursive: true, force: true });
266
- } catch (e) {
267
- // Ignore cleanup errors
268
- }
269
- }
270
- }
271
-
272
- install();