streamline package naming
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 bitsquare
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,59 @@
|
||||
# @bitsquare/nopy-cubes-core
|
||||
|
||||
The core cube bundle for [nopy](https://www.npmjs.com/package/@bitsquare/nopy):
|
||||
base packages, users, SSH, firewalling, networking, web serving and runtimes.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
pnpm add -D @bitsquare/nopy-cubes-core
|
||||
```
|
||||
|
||||
Then name it in `.nopyrc.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hosts": ["web-1"],
|
||||
"cubePackages": ["@bitsquare/nopy-cubes-core"]
|
||||
}
|
||||
```
|
||||
|
||||
`nopy` resolves the package from the directory of the config file that named it
|
||||
and scans its `cubes/` directory exactly as it scans a `cubeDirs` entry. Nothing
|
||||
has to be linked or copied.
|
||||
|
||||
## What is in it
|
||||
|
||||
| Area | Cube ids |
|
||||
| ---------- | ------------------------------------------------------------------- |
|
||||
| admin | `admin:cockpit`, `admin:hostname`, `admin:locale` |
|
||||
| packages | `apt:essentials`, `apt:install` |
|
||||
| hardening | `armor:fail2ban`, `armor:ssh`, `armor:ufw` |
|
||||
| web | `caddy`, `caddy:spa` |
|
||||
| source | `git:clone` |
|
||||
| networking | `net:tailscale`, `net:wifi:access-point`, `net:wifi:connection` |
|
||||
| runtimes | `runtime:docker`, `runtime:nodevm` |
|
||||
| services | `service:autostart` |
|
||||
| ssh | `ssh:authorize`, `ssh:keygen`, `ssh:keyman` |
|
||||
| users | `user:add`, `user:edit` |
|
||||
|
||||
Run `nopy` and pick from the list, or `nopy -P` to print the pyinfra commands
|
||||
without executing them. Each cube directory has its own `README.md`.
|
||||
|
||||
## Cube ids are global
|
||||
|
||||
An id such as `apt:essentials` is claimed repo-wide, not per bundle: two cubes
|
||||
with the same id — whichever sources they came from — abort the run with an
|
||||
error naming both. Prefix your own cubes distinctly if you also point
|
||||
`cubeDirs` at a local tree.
|
||||
|
||||
## The bundle is read-only
|
||||
|
||||
Under pnpm the installed files are hardlinked into the global store, so a cube
|
||||
that writes next to its own `deploy.py` corrupts that store for every project on
|
||||
the machine. Cubes here write to `/tmp` or to the remote host, never to their
|
||||
own directory.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,76 @@
|
||||
# cockpit
|
||||
|
||||
**Install Cockpit web-based server management interface**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs Cockpit, a powerful web-based interface for managing Linux servers, making system administration accessible through your browser.
|
||||
|
||||
## What is Cockpit?
|
||||
|
||||
Cockpit is a modern, interactive server admin interface that runs in your web browser. It provides:
|
||||
|
||||
- **Real-time monitoring**: CPU, memory, disk, and network usage graphs
|
||||
- **Container management**: View and manage Docker containers
|
||||
- **Service management**: Start, stop, and manage systemd services
|
||||
- **Storage administration**: Manage disks, RAID, and filesystems
|
||||
- **Network configuration**: Configure network interfaces and firewall
|
||||
- **Terminal access**: Built-in terminal for command-line access
|
||||
- **User management**: Create and manage user accounts
|
||||
- **Software updates**: View and apply system updates
|
||||
|
||||
Think of it as a control panel for your Linux server - all accessible from any web browser.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Installs the `cockpit` package
|
||||
2. Installs `sscg` (Simple Signed Certificate Generator) for HTTPS support
|
||||
3. Starts the Cockpit service
|
||||
4. Makes Cockpit accessible on port 9090
|
||||
|
||||
## Configuration
|
||||
|
||||
This cube currently has no configurable parameters.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Post-Installation
|
||||
|
||||
Access Cockpit by navigating to:
|
||||
```
|
||||
https://your-server-ip:9090
|
||||
```
|
||||
|
||||
Login with any valid system user account (e.g., root or a user created with the `user-add` cube).
|
||||
|
||||
### Security Notes
|
||||
|
||||
- Cockpit uses HTTPS by default (self-signed certificate)
|
||||
- Your browser will show a security warning on first access (expected with self-signed certs)
|
||||
- Consider using UFW to restrict access: `sudo ufw allow from YOUR_IP to any port 9090`
|
||||
- Disable Cockpit when not in use: `sudo systemctl stop cockpit.socket`
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- Monitor server performance in real-time
|
||||
- Manage Docker containers without command-line
|
||||
- View system logs and troubleshoot issues
|
||||
- Configure network settings
|
||||
- Apply system updates
|
||||
- Manage storage and filesystems
|
||||
|
||||
## Managing Cockpit
|
||||
|
||||
Start/stop Cockpit:
|
||||
```bash
|
||||
sudo systemctl start cockpit.socket
|
||||
sudo systemctl stop cockpit.socket
|
||||
sudo systemctl status cockpit.socket
|
||||
```
|
||||
|
||||
Disable Cockpit from starting on boot:
|
||||
```bash
|
||||
sudo systemctl disable cockpit.socket
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, apt
|
||||
|
||||
apt.packages(
|
||||
packages=[ "sscg cockpit"],
|
||||
present=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
server.service(
|
||||
'cockpit',
|
||||
running=True,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
|
||||
export default Manifest({
|
||||
id: 'admin:cockpit',
|
||||
name: 'Install cockpit and utils',
|
||||
dependencies: () => [],
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, files
|
||||
|
||||
"""
|
||||
Deployment script for the admin:hostname cube.
|
||||
Uses pyinfra's server.hostname operation to set and persist the system hostname.
|
||||
"""
|
||||
|
||||
HOSTNAME = host.data.HOSTNAME
|
||||
|
||||
if HOSTNAME:
|
||||
server.hostname(
|
||||
name=f"Set system hostname to {HOSTNAME}",
|
||||
hostname=HOSTNAME,
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
# 2. Update /etc/hosts to prevent "unable to resolve host" errors
|
||||
# This looks for the line starting with 127.0.1.1 and replaces it entirely
|
||||
files.line(
|
||||
name="Update /etc/hosts for local resolution",
|
||||
path="/etc/hosts",
|
||||
line=r"^127\.0\.1\.1\s+.*",
|
||||
replace=f"127.0.1.1 {HOSTNAME}",
|
||||
_sudo=True,
|
||||
)
|
||||
# 3. Restart Avahi (Network Broadcast)
|
||||
# This pushes the name change out to the shared network
|
||||
server.service(
|
||||
name="Restart Avahi to broadcast new mDNS name",
|
||||
service="avahi-daemon",
|
||||
restarted=True,
|
||||
_sudo=True,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Manifest, uniqid } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Manifest for the admin:hostname cube.
|
||||
* This cube allows for setting and persistently changing the system's hostname.
|
||||
*/
|
||||
export default Manifest({
|
||||
id: 'admin:hostname',
|
||||
name: 'Permanently change the hostname',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
HOSTNAME: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64)
|
||||
.describe('The new hostname for the target host')
|
||||
.default(`host-${uniqid()}`),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
# Cube: admin/locale
|
||||
|
||||
Configures system keyboard layout permanently by updating `/etc/default/keyboard` and using `localectl`.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `LAYOUT` (string): Keyboard layout (e.g. "ch", "us", "de"). Default: "ch".
|
||||
- `MODEL` (string): Keyboard model. Default: "pc105".
|
||||
- `VARIANT` (string): Keyboard variant. Default: "".
|
||||
- `OPTIONS` (string): Keyboard options (comma separated). Default: "".
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
|
||||
export default Manifest({
|
||||
name: 'My Host Setup',
|
||||
dependencies: () => [
|
||||
['admin:locale', { LAYOUT: 'de' }]
|
||||
]
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, files
|
||||
|
||||
LAYOUT = host.data.LAYOUT
|
||||
MODEL = host.data.MODEL
|
||||
VARIANT = host.data.VARIANT
|
||||
OPTIONS = host.data.OPTIONS
|
||||
|
||||
# Update /etc/default/keyboard
|
||||
files.line(
|
||||
name="Update XKBMODEL in /etc/default/keyboard",
|
||||
path="/etc/default/keyboard",
|
||||
line=r'^XKBMODEL=.*',
|
||||
replace=f'XKBMODEL="{MODEL}"',
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
files.line(
|
||||
name="Update XKBLAYOUT in /etc/default/keyboard",
|
||||
path="/etc/default/keyboard",
|
||||
line=r'^XKBLAYOUT=.*',
|
||||
replace=f'XKBLAYOUT="{LAYOUT}"',
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
files.line(
|
||||
name="Update XKBVARIANT in /etc/default/keyboard",
|
||||
path="/etc/default/keyboard",
|
||||
line=r'^XKBVARIANT=.*',
|
||||
replace=f'XKBVARIANT="{VARIANT}"',
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
files.line(
|
||||
name="Update XKBOPTIONS in /etc/default/keyboard",
|
||||
path="/etc/default/keyboard",
|
||||
line=r'^XKBOPTIONS=.*',
|
||||
replace=f'XKBOPTIONS="{OPTIONS}"',
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
# Apply keyboard configuration
|
||||
server.shell(
|
||||
name="Apply keyboard setup",
|
||||
commands=["setupcon", "service keyboard-setup restart"],
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
# Set X11 keyboard layout using localectl if available
|
||||
server.shell(
|
||||
name="Set X11 keyboard layout using localectl",
|
||||
commands=[f"localectl set-x11-keymap {LAYOUT} {MODEL} '{VARIANT}' '{OPTIONS}'"],
|
||||
_sudo=True,
|
||||
_ignore_errors=True,
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'admin:locale',
|
||||
name: 'Configure system locale and keyboard layout',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
LAYOUT: z.string().describe('Keyboard layout (e.g. "ch", "us", "de")').default('ch'),
|
||||
MODEL: z.string().describe('Keyboard model').default('pc105'),
|
||||
VARIANT: z.string().describe('Keyboard variant').default(''),
|
||||
OPTIONS: z.string().describe('Keyboard options (comma separated)').default(''),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
# apt:essentials
|
||||
|
||||
**Install essential packages**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs a curated collection of essential development tools and utilities that are commonly needed for server environments and development workflows.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
Installs the following packages via apt:
|
||||
|
||||
- **fish** - User-friendly command-line shell with autosuggestions and syntax highlighting
|
||||
- **ranger** - Terminal-based file manager with vi key bindings
|
||||
- **golang-go** - Go programming language compiler and tools
|
||||
- **build-essential** - Essential compilation tools (gcc, g++, make, etc.)
|
||||
- **python3** - Python 3 interpreter and standard library
|
||||
- **pkg-config** - Helper tool for compiling applications and libraries
|
||||
- **age** - Modern file encryption tool with small explicit keys
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **UPDATE** (boolean, default: `true`)
|
||||
- If already installed, should packages be updated?
|
||||
- Set to `false` to skip package cache updates and only install missing packages
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Use Cases
|
||||
|
||||
This cube is ideal as a base dependency for other cubes that require:
|
||||
- Basic development tools
|
||||
- Modern shell environments
|
||||
- File encryption capabilities
|
||||
- Python or Go runtimes
|
||||
@@ -0,0 +1,18 @@
|
||||
from pyinfra.operations import apt
|
||||
from pyinfra import host
|
||||
|
||||
UPDATE = host.data.UPDATE
|
||||
|
||||
apt.packages(
|
||||
name='Install essentials',
|
||||
packages=[
|
||||
'fish',
|
||||
'build-essential',
|
||||
'python3',
|
||||
'pkg-config',
|
||||
'age'
|
||||
],
|
||||
update=UPDATE,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'apt:essentials',
|
||||
name: 'Install essential packages',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
UPDATE: z.boolean().describe('If already installed should packages be updated').default(true),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
# apt
|
||||
|
||||
**Install packages with apt**
|
||||
|
||||
## Purpose
|
||||
|
||||
This is a generic cube for installing custom packages via the apt package manager. It's useful when you need to install specific packages that aren't covered by other specialized cubes.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Optionally updates the apt package cache
|
||||
2. Installs the specified space-separated list of packages
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **UPDATE** (boolean, default: `true`)
|
||||
- Update package cache before installing
|
||||
- Set to `false` to skip updating and only install packages
|
||||
|
||||
- **PACKAGES** (string, default: `''`)
|
||||
- Space-separated list of packages to install
|
||||
- Example: `"vim git htop curl wget"`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Use Cases
|
||||
|
||||
Install development tools:
|
||||
```
|
||||
PACKAGES="vim git htop tmux"
|
||||
```
|
||||
|
||||
Install database clients:
|
||||
```
|
||||
PACKAGES="postgresql-client mysql-client redis-tools"
|
||||
```
|
||||
|
||||
Install system utilities:
|
||||
```
|
||||
PACKAGES="curl wget jq unzip zip"
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
When deploying this cube, you would typically configure it like:
|
||||
```javascript
|
||||
exec('apt', {
|
||||
PACKAGES: 'nginx certbot python3-certbot-nginx',
|
||||
UPDATE: true
|
||||
})
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Package names must match exact apt package names
|
||||
- Invalid package names will cause the installation to fail
|
||||
- Use `apt search <package>` to find package names
|
||||
- Some packages may require additional configuration after installation
|
||||
@@ -0,0 +1,31 @@
|
||||
from pyinfra.operations import apt
|
||||
from pyinfra import host
|
||||
|
||||
UPDATE = host.data.UPDATE
|
||||
PACKAGES = str(host.data.PACKAGES).split(' ')
|
||||
|
||||
apt.packages(
|
||||
name='Install essential packages',
|
||||
packages=[
|
||||
'htop',
|
||||
'age',
|
||||
'git',
|
||||
'curl',
|
||||
'nano',
|
||||
'wget',
|
||||
'ca-certificates',
|
||||
'ufw',
|
||||
"gnupg",
|
||||
"lsb-release"
|
||||
],
|
||||
update=UPDATE,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name='Install custom packages',
|
||||
packages=[p.strip() for p in PACKAGES if p],
|
||||
update=UPDATE,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'apt:install',
|
||||
name: 'Install packages with apt',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
UPDATE: z.boolean().describe('Update package cache before installing').default(false),
|
||||
PACKAGES: z
|
||||
.string()
|
||||
.describe('Space-separated list of packages to install')
|
||||
.default('vim htop curl'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
# armor-fail2ban
|
||||
|
||||
**Install and enable fail2ban**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs and configures Fail2ban, an intrusion prevention software that protects your server from brute-force attacks and unauthorized access attempts.
|
||||
|
||||
## What is Fail2ban?
|
||||
|
||||
Fail2ban monitors log files (e.g., `/var/log/auth.log`) for suspicious activity, such as repeated failed login attempts. When it detects malicious behavior patterns, it automatically:
|
||||
|
||||
- Bans the offending IP address by updating firewall rules
|
||||
- Prevents the attacker from making further connection attempts
|
||||
- Can send email notifications about bans (if configured)
|
||||
|
||||
Common use cases include:
|
||||
- Protecting SSH from brute-force password attacks
|
||||
- Blocking repeated failed login attempts on web applications
|
||||
- Preventing DoS attacks from specific IP addresses
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Installs the `fail2ban` package via apt
|
||||
2. Deploys a custom configuration file (`jail.local`) to `/etc/fail2ban/jail.local`
|
||||
3. Configures fail2ban with sensible defaults for common services
|
||||
|
||||
## Configuration
|
||||
|
||||
This cube currently has no configurable parameters. The default configuration is applied from the included `jail.local` file.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Notes
|
||||
|
||||
After deployment, you can:
|
||||
- Check fail2ban status: `sudo fail2ban-client status`
|
||||
- View banned IPs: `sudo fail2ban-client status sshd`
|
||||
- Unban an IP: `sudo fail2ban-client set sshd unbanip <IP_ADDRESS>`
|
||||
@@ -0,0 +1,18 @@
|
||||
from pyinfra.operations import apt, files, server
|
||||
|
||||
# Install Fail2ban
|
||||
apt.packages(
|
||||
name='Install Fail2ban',
|
||||
packages=['fail2ban'],
|
||||
update=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Configure Fail2ban
|
||||
files.put(
|
||||
name='Configure Fail2ban',
|
||||
src='jail.local',
|
||||
dest='/etc/fail2ban/jail.local',
|
||||
mode='0644',
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = ssh
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
maxretry = 3
|
||||
findtime = 600
|
||||
bantime = 3600
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
|
||||
export default Manifest({
|
||||
id: 'armor:fail2ban',
|
||||
name: 'Install and enable fail2ban',
|
||||
dependencies: () => [],
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
# armor-ssh
|
||||
|
||||
**Secure SSH server by disabling password authentication**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube hardens your SSH server configuration by disabling less secure authentication methods, enforcing SSH key-based authentication only.
|
||||
|
||||
## Why Disable Password Authentication?
|
||||
|
||||
Password-based SSH authentication is vulnerable to:
|
||||
- **Brute-force attacks**: Automated scripts trying millions of password combinations
|
||||
- **Dictionary attacks**: Guessing common passwords
|
||||
- **Credential stuffing**: Using leaked passwords from other breaches
|
||||
- **Weak passwords**: Users choosing easily guessable passwords
|
||||
|
||||
**SSH key authentication is more secure** because:
|
||||
- Keys are cryptographically strong (2048+ bit keys vs 8-12 character passwords)
|
||||
- Private keys never travel over the network
|
||||
- Immune to brute-force attacks
|
||||
- Can be protected with passphrases for additional security
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Disables challenge-response authentication**
|
||||
- Prevents keyboard-interactive authentication prompts
|
||||
|
||||
2. **Optionally disables password authentication** (default: enabled)
|
||||
- Forces users to authenticate with SSH keys only
|
||||
- Prevents password-based login attempts
|
||||
|
||||
3. **Optionally disables PAM** (Pluggable Authentication Modules)
|
||||
- Disables PAM-based authentication methods
|
||||
- Reduces attack surface
|
||||
|
||||
4. **Restarts SSH service**
|
||||
- Applies the new configuration immediately
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **DISABLE_PASSWORD** (boolean, default: `true`)
|
||||
- Disable password authentication for SSH connections
|
||||
- ⚠️ **WARNING**: Ensure you have SSH key access configured before enabling this!
|
||||
|
||||
- **DISABLE_PAM** (boolean, default: `true`)
|
||||
- Disable PAM (Pluggable Authentication Modules) for SSH
|
||||
- Recommended for key-only authentication setups
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
**Before deploying this cube**:
|
||||
1. Ensure you have SSH key authentication set up and tested
|
||||
2. Keep an alternative access method available (console access, VNC, etc.)
|
||||
3. Test SSH key login before disabling passwords
|
||||
4. Consider using the `user-add` or `ssh-keyman` cubes first
|
||||
|
||||
**After deployment**:
|
||||
- Only SSH key authentication will work
|
||||
- Password login attempts will be rejected
|
||||
- Make sure to back up your private SSH key securely
|
||||
|
||||
## Post-Installation
|
||||
|
||||
The SSH service will restart automatically. Your current SSH session will remain active, but new connections must use SSH keys.
|
||||
|
||||
To verify the configuration:
|
||||
```bash
|
||||
sudo grep -E "PasswordAuthentication|ChallengeResponseAuthentication|UsePAM" /etc/ssh/sshd_config
|
||||
```
|
||||
|
||||
## Recovery
|
||||
|
||||
If you get locked out:
|
||||
1. Access the server via console (physical or cloud provider's web console)
|
||||
2. Edit `/etc/ssh/sshd_config`
|
||||
3. Set `PasswordAuthentication yes`
|
||||
4. Restart SSH: `sudo systemctl restart ssh`
|
||||
@@ -0,0 +1,43 @@
|
||||
from pyinfra.operations import files, server
|
||||
from pyinfra import host
|
||||
from pyinfra import config
|
||||
import logging
|
||||
|
||||
DISABLE_PASSWORD=host.data.DISABLE_PASSWORD
|
||||
DISABLE_PAM=host.data.DISABLE_PAM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
config.SUDO = True
|
||||
|
||||
files.line(
|
||||
name='Disable challenge-response authentication in SSH',
|
||||
path='/etc/ssh/sshd_config',
|
||||
line='ChallengeResponseAuthentication yes',
|
||||
replace='ChallengeResponseAuthentication no',
|
||||
)
|
||||
|
||||
if DISABLE_PASSWORD:
|
||||
files.line(
|
||||
name='Disable password authentication in SSH',
|
||||
path='/etc/ssh/sshd_config',
|
||||
line='PasswordAuthentication yes',
|
||||
replace='PasswordAuthentication no',
|
||||
)
|
||||
else:
|
||||
logger.info('Password authentication allowed')
|
||||
|
||||
if DISABLE_PAM:
|
||||
files.line(
|
||||
name='Disable PAM in SSH',
|
||||
path='/etc/ssh/sshd_config',
|
||||
line='UsePAM yes',
|
||||
replace='UsePAM no',
|
||||
)
|
||||
|
||||
# Restart SSH service
|
||||
server.service(
|
||||
'ssh',
|
||||
running=True,
|
||||
restarted=True,
|
||||
reloaded=True
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'armor:ssh',
|
||||
name: 'Secure SSH server by disabling password authentication',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
DISABLE_PASSWORD: z
|
||||
.boolean()
|
||||
.describe('Disable password authentication for SSH connections')
|
||||
.default(true),
|
||||
DISABLE_PAM: z
|
||||
.boolean()
|
||||
.describe('Disable PAM (Pluggable Authentication Modules) for SSH')
|
||||
.default(true),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
# armor-ufw
|
||||
|
||||
**Activate UFW (Uncomplicated Firewall)**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube configures and enables UFW, a user-friendly firewall management tool for Linux systems, providing basic protection against unauthorized network access.
|
||||
|
||||
## What is UFW?
|
||||
|
||||
UFW (Uncomplicated Firewall) is a frontend for `iptables` designed to make firewall configuration simple and accessible. It provides:
|
||||
|
||||
- **Easy-to-understand syntax**: Commands like `ufw allow ssh` instead of complex iptables rules
|
||||
- **Default deny policy**: Blocks all incoming connections except those explicitly allowed
|
||||
- **Connection tracking**: Automatically handles related and established connections
|
||||
- **Application profiles**: Pre-configured rules for common services
|
||||
|
||||
Think of UFW as a security gate for your server - it controls which network traffic is allowed in and out.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Configures UFW to allow SSH connections (port 22)
|
||||
- Ensures you don't lock yourself out when enabling the firewall
|
||||
2. Optionally allows HTTP traffic (port 80) based on the `ALLOW_HTTP` parameter
|
||||
3. Enables the firewall with the configured rules
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **ALLOW_HTTP** (boolean, default: `true`)
|
||||
- Allow incoming HTTP traffic on port 80
|
||||
- Set to `false` if you're only using HTTPS or don't need web traffic
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **apt:essentials** - Required for basic system tools
|
||||
|
||||
## Security Notes
|
||||
|
||||
**Important**: This cube automatically allows SSH to prevent lockouts. If you need to allow additional services, you can run:
|
||||
|
||||
```bash
|
||||
sudo ufw allow [port number]/[protocol]
|
||||
sudo ufw allow [service-name]
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `sudo ufw allow 443/tcp` - Allow HTTPS
|
||||
- `sudo ufw allow 3000/tcp` - Allow custom application port
|
||||
- `sudo ufw allow https` - Allow HTTPS by service name
|
||||
|
||||
## Post-Installation
|
||||
|
||||
Check firewall status:
|
||||
```bash
|
||||
sudo ufw status verbose
|
||||
sudo ufw status numbered
|
||||
```
|
||||
|
||||
Common UFW commands:
|
||||
- Delete rule: `sudo ufw delete [rule number]`
|
||||
- Disable firewall: `sudo ufw disable`
|
||||
- Reset to defaults: `sudo ufw reset`
|
||||
|
||||
## UFW File Locations
|
||||
|
||||
UFW rules are stored in the `/etc/ufw` directory:
|
||||
|
||||
- `/etc/ufw/user.rules` - Custom rules added via the `ufw` command
|
||||
- `/etc/ufw/before.rules` - Rules processed before user rules (high priority)
|
||||
- `/etc/ufw/after.rules` - Rules processed after user rules (exceptions)
|
||||
- `/etc/ufw/sysctl.conf` - Kernel network parameters (e.g., packet forwarding)
|
||||
- `/etc/ufw/applications.d/` - Application profiles for common services
|
||||
- `/etc/default/ufw` - Global UFW settings and default policies
|
||||
|
||||
Understanding these locations is helpful for troubleshooting, manual edits, or backing up your firewall configuration.
|
||||
@@ -0,0 +1,13 @@
|
||||
from pyinfra.operations import server
|
||||
from pyinfra import host
|
||||
|
||||
ALLOW_HTTP=host.data.ALLOW_HTTP
|
||||
|
||||
server.shell(
|
||||
commands=[
|
||||
f"ufw allow ssh",
|
||||
f"ufw allow http" if ALLOW_HTTP else "",
|
||||
f"ufw enable",
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'armor:ufw',
|
||||
name: 'Activate ufw (uncomplicated firewall)',
|
||||
dependencies: () => ['apt:essentials'],
|
||||
schema: z.object({
|
||||
ALLOW_HTTP: z.boolean().describe('Allow incoming HTTP traffic on port 80').default(true),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
# caddy
|
||||
|
||||
**Install Caddy webserver**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs Caddy, a modern, powerful web server with automatic HTTPS that's designed to be easy to use and configure.
|
||||
|
||||
## What is Caddy?
|
||||
|
||||
Caddy is a next-generation web server that stands out for its simplicity and built-in security:
|
||||
|
||||
- **Automatic HTTPS**: Automatically obtains and renews SSL/TLS certificates from Let's Encrypt
|
||||
- **Modern HTTP features**: HTTP/2, HTTP/3 (QUIC) support out of the box
|
||||
- **Simple configuration**: Human-readable Caddyfile format
|
||||
- **Reverse proxy**: Easy proxying to backend applications
|
||||
- **Static file serving**: Fast and efficient static site hosting
|
||||
- **Zero-downtime reloads**: Update config without dropping connections
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Adds Caddy's official repository**
|
||||
- Installs required dependencies (debian-keyring, apt-transport-https, curl)
|
||||
- Downloads and installs Caddy's GPG signing key
|
||||
- Configures APT to use Caddy's official stable repository
|
||||
|
||||
2. **Installs Caddy**
|
||||
- Installs the latest stable version of Caddy
|
||||
- Sets up the Caddy service
|
||||
|
||||
3. **Configures TLS**
|
||||
- Creates a Caddyfile with a reusable TLS snippet
|
||||
- Configures TLS based on the `TLS` parameter
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **TLS** (string, default: `''`)
|
||||
- TLS certificate configuration
|
||||
- **Options**:
|
||||
- `''` (empty string) - Automatic HTTPS with Let's Encrypt (recommended)
|
||||
- `'internal'` - Use Caddy's internal CA for self-signed certs (testing only)
|
||||
- `'/path/to/cert /path/to/key'` - Provide custom certificate paths
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## TLS Configuration Examples
|
||||
|
||||
**Automatic HTTPS (Production)**:
|
||||
```javascript
|
||||
exec('caddy', { TLS: '' })
|
||||
```
|
||||
Caddy will automatically obtain SSL certificates from Let's Encrypt for your domain.
|
||||
|
||||
**Self-Signed for Testing**:
|
||||
```javascript
|
||||
exec('caddy', { TLS: 'internal' })
|
||||
```
|
||||
Uses Caddy's internal CA. Browsers will show security warnings.
|
||||
|
||||
**Custom Certificates**:
|
||||
```javascript
|
||||
exec('caddy', { TLS: '/etc/ssl/certs/mycert.pem /etc/ssl/private/mykey.pem' })
|
||||
```
|
||||
Use your own certificate and private key files.
|
||||
|
||||
## Post-Installation
|
||||
|
||||
The Caddyfile is created at `/etc/caddy/Caddyfile` with a reusable TLS snippet:
|
||||
|
||||
```
|
||||
(tls_cert) {
|
||||
tls {TLS}
|
||||
}
|
||||
```
|
||||
|
||||
Other cubes (like `caddy-spa`) can import this snippet with `import tls_cert`.
|
||||
|
||||
## Managing Caddy
|
||||
|
||||
Start/stop/restart Caddy:
|
||||
```bash
|
||||
sudo systemctl start caddy
|
||||
sudo systemctl stop caddy
|
||||
sudo systemctl restart caddy
|
||||
sudo systemctl status caddy
|
||||
```
|
||||
|
||||
Reload configuration without downtime:
|
||||
```bash
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
Test configuration:
|
||||
```bash
|
||||
caddy validate --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- Reverse proxy for Node.js/Python/Go apps
|
||||
- Static website hosting
|
||||
- API gateway
|
||||
- Load balancer
|
||||
- SSL/TLS termination
|
||||
|
||||
## Notes
|
||||
|
||||
- Caddy runs on ports 80 (HTTP) and 443 (HTTPS) by default
|
||||
- Ensure these ports are open in your firewall (UFW)
|
||||
- For automatic HTTPS, your domain must point to your server's IP
|
||||
- Caddy automatically redirects HTTP to HTTPS when using automatic HTTPS
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Caddy Documentation](https://caddyserver.com/docs/)
|
||||
- [Caddyfile Tutorial](https://caddyserver.com/docs/caddyfile-tutorial)
|
||||
@@ -0,0 +1,34 @@
|
||||
from pyinfra.operations import apt, server, files
|
||||
from pyinfra import host
|
||||
from io import StringIO
|
||||
|
||||
# 🔹 Variables
|
||||
TLS = host.data.TLS
|
||||
|
||||
server.shell(
|
||||
commands=[
|
||||
f"sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl",
|
||||
f"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg",
|
||||
f"curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list"
|
||||
]
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name='Install caddy',
|
||||
packages=['caddy'],
|
||||
update=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
|
||||
TLS_BLOCK = f"""
|
||||
(tls_cert) {{
|
||||
tls {TLS}
|
||||
}}
|
||||
"""
|
||||
|
||||
files.put(
|
||||
src = StringIO(TLS_BLOCK), # local filename to upload,
|
||||
dest = '/etc/caddy/Caddyfile', # the remote filename to upload to
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'caddy',
|
||||
name: 'Install Caddy webserver',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
// tls /path/to/cert.pem /path/to/key.pem
|
||||
TLS: z
|
||||
.string()
|
||||
.default('')
|
||||
.describe(`
|
||||
<empty-string> --> enabled and automatically managed
|
||||
internal --> use Caddy custom root CA for self-signed certs (for testing purpose)
|
||||
/path/to/cert /path/to/key --> Provide custom certificates
|
||||
`),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
# caddy-spa
|
||||
|
||||
**Configure Caddy to serve a Single Page Application**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube adds a reverse proxy configuration to Caddy for serving a Single Page Application (SPA) from a local backend server, with automatic HTTPS support.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Adds domain configuration to Caddyfile**
|
||||
- Creates a site block for the specified domain
|
||||
- Configures reverse proxy to forward traffic to local application
|
||||
- Imports TLS configuration from the `caddy` cube
|
||||
|
||||
2. **Configures reverse proxy**
|
||||
- Proxies all requests to `localhost:{PORT}`
|
||||
- Preserves headers and client information
|
||||
- Handles WebSocket connections
|
||||
|
||||
3. **Restarts Caddy service**
|
||||
- Applies the new configuration immediately
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **DOMAIN** (string, default: `''`)
|
||||
- Domain name for the SPA application
|
||||
- Example: `'myapp.example.com'`
|
||||
- Must have DNS pointing to your server's IP
|
||||
|
||||
- **PORT** (number, default: `5432`)
|
||||
- Port number where the SPA will be served
|
||||
- Your application should be listening on this port locally
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **caddy** cube (implicitly required) - Must be installed first to provide the `tls_cert` snippet
|
||||
|
||||
## What Gets Configured
|
||||
|
||||
This cube adds the following to `/etc/caddy/Caddyfile`:
|
||||
|
||||
```
|
||||
# BEGIN DOMAIN myapp.example.com
|
||||
myapp.example.com {
|
||||
import tls_cert
|
||||
reverse_proxy localhost:5432
|
||||
}
|
||||
# END myapp.example.com
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
**Deploy a React/Vue/Angular app**:
|
||||
```javascript
|
||||
exec('caddy-spa', {
|
||||
DOMAIN: 'app.example.com',
|
||||
PORT: 3000
|
||||
})
|
||||
```
|
||||
|
||||
**Deploy multiple SPAs**:
|
||||
```javascript
|
||||
exec('caddy-spa', { DOMAIN: 'app1.example.com', PORT: 3000 })
|
||||
exec('caddy-spa', { DOMAIN: 'app2.example.com', PORT: 3001 })
|
||||
exec('caddy-spa', { DOMAIN: 'app3.example.com', PORT: 3002 })
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User visits `https://myapp.example.com`
|
||||
2. Caddy receives the request on port 443 (HTTPS)
|
||||
3. Caddy automatically handles SSL/TLS encryption
|
||||
4. Request is forwarded to `localhost:5432`
|
||||
5. Your application receives the request and returns a response
|
||||
6. Caddy sends the encrypted response back to the user
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before deploying this cube:
|
||||
|
||||
1. **Install the caddy cube first**
|
||||
- Provides the base Caddy installation and TLS configuration
|
||||
|
||||
2. **Ensure your application is running**
|
||||
- Your SPA backend should be listening on the specified PORT
|
||||
- Example: `npm start` or `pm2 start app.js`
|
||||
|
||||
3. **Configure DNS**
|
||||
- Point your domain's A record to your server's IP address
|
||||
- Wait for DNS propagation (can take a few minutes to hours)
|
||||
|
||||
4. **Open firewall ports**
|
||||
- Ensure ports 80 and 443 are open (for automatic HTTPS)
|
||||
- `sudo ufw allow 80/tcp`
|
||||
- `sudo ufw allow 443/tcp`
|
||||
|
||||
## Post-Installation
|
||||
|
||||
Verify the configuration:
|
||||
```bash
|
||||
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||
```
|
||||
|
||||
Check Caddy status:
|
||||
```bash
|
||||
sudo systemctl status caddy
|
||||
```
|
||||
|
||||
View Caddy logs:
|
||||
```bash
|
||||
sudo journalctl -u caddy -f
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
**502 Bad Gateway**:
|
||||
- Your application isn't running on the specified PORT
|
||||
- Check: `netstat -tlnp | grep {PORT}`
|
||||
|
||||
**Certificate errors**:
|
||||
- DNS not pointing to your server
|
||||
- Ports 80/443 blocked by firewall
|
||||
- Check Caddy logs: `sudo journalctl -u caddy -f`
|
||||
|
||||
**Domain not resolving**:
|
||||
- DNS propagation not complete yet
|
||||
- Verify with: `dig +short myapp.example.com`
|
||||
|
||||
## Notes
|
||||
|
||||
- Caddy automatically obtains and renews Let's Encrypt certificates
|
||||
- The reverse proxy preserves the original client IP and headers
|
||||
- WebSocket connections are automatically supported
|
||||
- You can add multiple domains by running this cube multiple times with different parameters
|
||||
@@ -0,0 +1,32 @@
|
||||
from pyinfra.operations import apt, server, files
|
||||
from pyinfra import host
|
||||
from io import StringIO
|
||||
|
||||
# 🔹 Variables (Modify as Needed)
|
||||
DOMAIN = host.data.DOMAIN
|
||||
PORT = host.data.PORT
|
||||
|
||||
|
||||
site_block = f"""# BEGIN DOMAIN {DOMAIN}
|
||||
{DOMAIN} {{
|
||||
import tls_cert
|
||||
reverse_proxy localhost:{PORT}
|
||||
}}
|
||||
# END {DOMAIN}
|
||||
"""
|
||||
|
||||
files.block(
|
||||
path = '/etc/caddy/Caddyfile',
|
||||
content = site_block,
|
||||
present = True,
|
||||
before = False,
|
||||
after = False,
|
||||
_sudo = True
|
||||
)
|
||||
|
||||
server.service(
|
||||
service='caddy',
|
||||
running=True,
|
||||
restarted=True,
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'caddy:spa',
|
||||
name: 'Install single page application',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
DOMAIN: z.string().describe('Domain name for the SPA application').default(''),
|
||||
PORT: z.number().describe('Port number where the SPA will be served').default(5432),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
from pyinfra.operations import server
|
||||
from pyinfra import host
|
||||
|
||||
USER = host.data.USER
|
||||
REPO = host.data.REPO
|
||||
APP = host.data.APP
|
||||
|
||||
server.shell(
|
||||
name="Clone application",
|
||||
commands=[
|
||||
f"""
|
||||
cd $HOME &&
|
||||
git clone --recurse-submodules {REPO} {APP}
|
||||
"""],
|
||||
_sudo=True,
|
||||
_sudo_user=USER,
|
||||
_use_sudo_login=True
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'git:clone',
|
||||
name: 'Clone a repository',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
USER: z.string().describe('Username for which to clone the repository').default('vagrant'),
|
||||
REPO: z.string().default(''),
|
||||
APP: z.string().describe('The internal name used for this application').default(''),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
# Tailscale Cube
|
||||
|
||||
Installs and authenticates the Tailscale client on a Linux host.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automated Installation**: Adds the official Tailscale repository and installs the package.
|
||||
- **Headless Authentication**: Uses a Tailscale Auth Key for zero-interaction setup.
|
||||
- **Headscale Support**: Can be configured to connect to a custom login server.
|
||||
- **Startup persistence**: Ensures the `tailscaled` daemon is enabled and running.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `AUTH_KEY` | `""` | **Secret.** Tailscale Auth Key (recommended to use a 'reusable' or 'ephemeral' key). |
|
||||
| `LOGIN_SERVER` | `https://controlplane.tailscale.com` | The coordination server URL. Set this to your Headscale instance URL if applicable. |
|
||||
| `EXTRA_ARGS` | `""` | Additional flags to pass to `tailscale up` (e.g., `--advertise-exit-node`). |
|
||||
| `FORCE_REAUTH` | `false` | If true, forces the client to re-authenticate. |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
nopy install tailscale
|
||||
```
|
||||
|
||||
When prompted, provide your `AUTH_KEY`. If you are using Headscale, also provide the `LOGIN_SERVER` URL.
|
||||
|
||||
`AUTH_KEY` is declared in the manifest's `secrets`, so nopy keeps it out of session
|
||||
and history files and masks it in any command it prints. It is asked for again on
|
||||
replay, and a `--use-defaults` replay refuses rather than joining the tailnet with
|
||||
an empty key. Prefer an ephemeral key regardless — the value is still on pyinfra's
|
||||
command line while the deployment runs.
|
||||
@@ -0,0 +1,45 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, apt
|
||||
|
||||
# Variables from manifest
|
||||
AUTH_KEY = host.data.AUTH_KEY
|
||||
LOGIN_SERVER = host.data.LOGIN_SERVER
|
||||
EXTRA_ARGS = host.data.EXTRA_ARGS
|
||||
FORCE_REAUTH = host.data.FORCE_REAUTH
|
||||
|
||||
# 1. Install Tailscale using the official one-liner script
|
||||
server.shell(
|
||||
name="Install Tailscale",
|
||||
commands=["curl -fsSL https://tailscale.com/install.sh | sh"],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# 2. Ensure Tailscale is enabled and running
|
||||
# Manually (Linux): sudo systemctl enable --now tailscaled
|
||||
server.service(
|
||||
name="Ensure tailscaled is running and enabled on boot",
|
||||
service="tailscaled",
|
||||
running=True,
|
||||
enabled=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# 3. Authenticate and bring Tailscale up
|
||||
# We use --authkey for headless mode
|
||||
# We use --login-server if it's different from the default
|
||||
up_command = f"tailscale up --authkey {AUTH_KEY}"
|
||||
|
||||
if LOGIN_SERVER and LOGIN_SERVER != "https://controlplane.tailscale.com":
|
||||
up_command += f" --login-server {LOGIN_SERVER}"
|
||||
|
||||
if FORCE_REAUTH:
|
||||
up_command += " --force-reauth"
|
||||
|
||||
if EXTRA_ARGS:
|
||||
up_command += f" {EXTRA_ARGS}"
|
||||
|
||||
server.shell(
|
||||
name="Authenticate Tailscale (Headless)",
|
||||
commands=[up_command],
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'net:tailscale',
|
||||
name: 'Install and authenticate Tailscale',
|
||||
dependencies: () => ['apt:essentials'],
|
||||
secrets: ['AUTH_KEY'],
|
||||
schema: z.object({
|
||||
AUTH_KEY: z.string().describe('Tailscale Auth Key for headless authentication').default(''),
|
||||
LOGIN_SERVER: z
|
||||
.string()
|
||||
.describe('Custom login server (e.g., for Headscale)')
|
||||
.default('https://controlplane.tailscale.com'),
|
||||
EXTRA_ARGS: z.string().describe('Additional arguments for tailscale up').default(''),
|
||||
FORCE_REAUTH: z.boolean().describe('Force re-authentication').default(false),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,243 @@
|
||||
# WiFi Access Point Cube (NetworkManager)
|
||||
|
||||
Configures a Linux device as a WiFi Access Point using NetworkManager's `nmcli` command. **This approach allows the device to simultaneously act as an AP AND remain connected to another WiFi network as a client.**
|
||||
|
||||
## Key Features
|
||||
|
||||
- ✅ **Dual WiFi Mode**: Acts as AP while staying connected to another WiFi
|
||||
- ✅ **Modern Approach**: Uses NetworkManager (nmcli) instead of hostapd
|
||||
- ✅ **Auto-start**: Configured to start on boot
|
||||
- ✅ **IP Sharing**: Built-in internet sharing with `ipv4.method shared`
|
||||
- ✅ **Simple Configuration**: Minimal parameters, maximum functionality
|
||||
- ✅ **Supports 2.4GHz and 5GHz bands**
|
||||
|
||||
## Requirements
|
||||
|
||||
- Device with WiFi capability (e.g., Raspberry Pi 4/5)
|
||||
- NetworkManager installed and running
|
||||
- WiFi hardware that supports AP mode
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
> **This section is out of date** — it lists parameters the manifest does not
|
||||
> declare (`NETWORK_DEVICE`, `CHANNEL`, `IP_ADDRESS`) and omits `AP_IP`. Read
|
||||
> `manifest.mjs` for the real list. Tracked as §5 in the repository's
|
||||
> `DOCS-AUDIT.md`.
|
||||
|
||||
### Prompted first
|
||||
|
||||
- **SSID**: WiFi network name (1-32 characters). Defaults to `PiPoint`.
|
||||
- **PASSWORD**: WPA2 password (8-63 characters). Defaults to `1223334444` — a
|
||||
placeholder that should not survive contact with a real network.
|
||||
Declared in the manifest's `secrets`, so nopy keeps it out of session and
|
||||
history files and masks it in printed commands, and re-prompts on replay. The
|
||||
value is still on pyinfra's command line, so it is visible in `ps` during the run.
|
||||
|
||||
### Optional (with defaults)
|
||||
|
||||
- **NETWORK_DEVICE**: WiFi interface to use (`wlan0` default)
|
||||
- **BAND**: Frequency band - `2.4GHz` or `5GHz` (auto-detected from current connection if not specified)
|
||||
- **CHANNEL**: WiFi channel (auto-detected from current connection if not specified)
|
||||
- 2.4GHz: 1-14
|
||||
- 5GHz: 36-165
|
||||
- **IP_ADDRESS**: AP gateway address (`192.168.50.1` default)
|
||||
- **CONNECTION_NAME**: NetworkManager connection name (`net:wifi:ap` default)
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"SSID": "MyHomeWiFi",
|
||||
"PASSWORD": "SecurePass123"
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration (5GHz with specific channel)
|
||||
|
||||
```json
|
||||
{
|
||||
"SSID": "FastWiFi5G",
|
||||
"PASSWORD": "SuperSecure456",
|
||||
"NETWORK_DEVICE": "wlan1",
|
||||
"BAND": "5GHz",
|
||||
"CHANNEL": 36,
|
||||
"IP_ADDRESS": "10.0.0.1",
|
||||
"CONNECTION_NAME": "my-hotspot"
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Detection Configuration
|
||||
|
||||
When `BAND` and `CHANNEL` are not specified, the script will automatically detect the current AP's band and channel using `iw dev <interface> link` and use those values for the new hotspot. This is useful when you want the hotspot to operate on the same band/channel as your existing connection to avoid interference.
|
||||
|
||||
```json
|
||||
{
|
||||
"SSID": "MyAutoAP",
|
||||
"PASSWORD": "SecurePass123"
|
||||
}
|
||||
```
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Installs NetworkManager** (if not present)
|
||||
2. **Enables NetworkManager service** and ensures it's running
|
||||
3. **Auto-detects current AP settings** using `iw dev <interface> link` to find the band and channel
|
||||
4. **Creates WiFi hotspot** using `nmcli` with auto-detected or specified settings
|
||||
5. **Configures IP sharing** with `ipv4.method shared` (automatic DHCP + NAT)
|
||||
6. **Enables IP forwarding** for internet routing
|
||||
7. **Sets autoconnect** so the AP starts on boot
|
||||
|
||||
## How It Works (Dual WiFi Mode)
|
||||
|
||||
### Simultaneous AP + Client Mode
|
||||
|
||||
**Hardware Support Required:**
|
||||
|
||||
- Your WiFi hardware must support **simultaneous AP+STA (Station) mode**
|
||||
- Most modern WiFi chips support this (e.g., Raspberry Pi 4/5, Intel WiFi cards)
|
||||
- Older hardware may not support it (e.g., Raspberry Pi 3B and earlier have limitations)
|
||||
|
||||
**How NetworkManager Handles It:**
|
||||
|
||||
1. **Single Interface (e.g., wlan0):**
|
||||
- If hardware supports AP+STA: ✅ **Both client and AP run on same interface**
|
||||
- If hardware doesn't support it: ⚠️ **Client connection may be dropped**
|
||||
|
||||
2. **Multiple Interfaces (e.g., wlan0 + wlan1):**
|
||||
- NetworkManager will use one for client, another for AP
|
||||
- Always works regardless of hardware capabilities
|
||||
|
||||
**Example Scenario (Raspberry Pi 4):**
|
||||
|
||||
```
|
||||
[Internet] <--WiFi--> [RPi4 wlan0 (Client+AP)] <--WiFi--> [Devices connect to AP]
|
||||
```
|
||||
|
||||
**How to Check Hardware Support:**
|
||||
|
||||
```bash
|
||||
iw list | grep -A 10 "valid interface combinations"
|
||||
```
|
||||
|
||||
Look for: `* #{ managed } <= 1, #{ AP } <= 1` or `* #{ managed, AP } <= 2`
|
||||
|
||||
NetworkManager will:
|
||||
|
||||
- Share internet from **any** available source (WiFi client, Ethernet, cellular, etc.)
|
||||
- Automatically handle routing and NAT
|
||||
- Try to maintain client connection if hardware supports it
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After deployment, the device will:
|
||||
|
||||
- ✅ Broadcast the WiFi network with your SSID
|
||||
- ✅ Accept connections with your password
|
||||
- ✅ Assign IP addresses to connected clients (via built-in DHCP)
|
||||
- ✅ Share internet connection from any available interface
|
||||
- ✅ Auto-start the AP on every boot
|
||||
- ✅ Maintain client WiFi connection (if connected to another network)
|
||||
|
||||
## Managing the Hotspot
|
||||
|
||||
### View connection status
|
||||
|
||||
```bash
|
||||
nmcli connection show
|
||||
```
|
||||
|
||||
### Stop the hotspot
|
||||
|
||||
```bash
|
||||
sudo nmcli connection down net:wifi:ap
|
||||
```
|
||||
|
||||
### Start the hotspot
|
||||
|
||||
```bash
|
||||
sudo nmcli connection up net:wifi:ap
|
||||
```
|
||||
|
||||
### Disable autostart
|
||||
|
||||
```bash
|
||||
sudo nmcli connection modify net:wifi:ap connection.autoconnect no
|
||||
```
|
||||
|
||||
### Delete the hotspot
|
||||
|
||||
```bash
|
||||
sudo nmcli connection delete net:wifi:ap
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### AP doesn't start
|
||||
|
||||
- Check NetworkManager status: `sudo systemctl status NetworkManager`
|
||||
- Verify WiFi interface exists: `nmcli device status`
|
||||
- Check if AP mode is supported: `iw list | grep -A 10 "Supported interface modes"`
|
||||
|
||||
### Can't connect to AP
|
||||
|
||||
- Verify password is correct (8+ characters)
|
||||
- Check channel compatibility with your devices
|
||||
- Try switching between 2.4GHz and 5GHz bands
|
||||
|
||||
### No internet on clients
|
||||
|
||||
- Verify host device has internet: `ping 8.8.8.8`
|
||||
- Check IP forwarding: `sysctl net.ipv4.ip_forward`
|
||||
- NetworkManager should handle NAT automatically with `ipv4.method shared`
|
||||
|
||||
### Hotspot conflicts with client WiFi
|
||||
|
||||
- This shouldn't happen with NetworkManager
|
||||
- If it does, check if hardware supports simultaneous AP+STA mode:
|
||||
|
||||
```bash
|
||||
iw list | grep "valid interface combinations"
|
||||
```
|
||||
|
||||
## Advantages Over Traditional Approach
|
||||
|
||||
| Feature | Traditional (hostapd) | NetworkManager (nmcli) |
|
||||
|---------|----------------------|------------------------|
|
||||
| **Dual WiFi** | ❌ No (conflicts with wpa_supplicant) | ✅ Yes (AP + client simultaneously) |
|
||||
| **Configuration** | Complex (multiple files) | Simple (one command) |
|
||||
| **DHCP** | Manual (dnsmasq) | Automatic |
|
||||
| **NAT** | Manual (iptables) | Automatic |
|
||||
| **Management** | Multiple services | Single service |
|
||||
| **Dependencies** | hostapd, dnsmasq, iptables | NetworkManager only |
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Always use a strong password (minimum 8 characters)
|
||||
- WPA2 encryption is automatically enabled
|
||||
- NetworkManager handles firewall rules automatically
|
||||
|
||||
## Compatibility
|
||||
|
||||
Tested on:
|
||||
|
||||
- Raspberry Pi 4/5 with built-in WiFi
|
||||
- Ubuntu/Debian-based systems with NetworkManager
|
||||
- Devices with WiFi hardware supporting AP mode
|
||||
|
||||
**Note**: Older Raspberry Pi models (3B and earlier) may have limitations with simultaneous AP + client mode due to hardware constraints.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
|
||||
|
||||
sudo nmcli con add type wifi con-name MyHotspot ifname wlan0 mode ap ssid YourNewAP ipv4.method shared wifi-sec.key-mgmt wpa-psk wifi-sec.psk "YourNewPassword"
|
||||
sudo nmcli con up MyHotspot
|
||||
sudo nmcli con down MyHotspot
|
||||
sudo nmcli con mod MyHotspot wifi.band <band> wifi.channel <channel_number>
|
||||
# Find the currently used band and channel
|
||||
iw dev wlan0 link
|
||||
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
from pyinfra.operations import apt, server, systemd
|
||||
from pyinfra import host
|
||||
|
||||
# Extract variables with proper defaults
|
||||
SSID = host.data.get('SSID')
|
||||
PASSWORD = host.data.get('PASSWORD')
|
||||
BAND = host.data.get('BAND', '2.4GHz')
|
||||
CHANNEL = host.data.get('CHANNEL') # Optional
|
||||
AP_IP = host.data.get('AP_IP')
|
||||
CONNECTION_NAME = host.data.get('CONNECTION_NAME')
|
||||
|
||||
# Determine band configuration
|
||||
if BAND == '5GHz':
|
||||
BAND = 'a'
|
||||
default_channel = 36 if not CHANNEL else CHANNEL
|
||||
else: # 2.4GHz
|
||||
BAND = 'bg'
|
||||
default_channel = 6 if not CHANNEL else CHANNEL
|
||||
|
||||
# Install NetworkManager if not present
|
||||
apt.packages(
|
||||
name='Install NetworkManager',
|
||||
packages=['network-manager'],
|
||||
update=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Ensure NetworkManager is running
|
||||
systemd.service(
|
||||
name='Ensure NetworkManager is running',
|
||||
service='NetworkManager',
|
||||
running=True,
|
||||
enabled=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Check if connection already exists and delete it
|
||||
server.shell(
|
||||
name='Delete existing AP connection if present',
|
||||
commands=[f'nmcli connection delete {CONNECTION_NAME} || true'],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Add Wifi AP connection
|
||||
server.shell(
|
||||
name='Detect WiFi interface',
|
||||
commands=[
|
||||
f'sudo nmcli con add type wifi con-name {CONNECTION_NAME} ifname wlan0 mode ap ssid {SSID} ipv4.method shared wifi-sec.key-mgmt wpa-psk wifi-sec.psk "{PASSWORD}"',
|
||||
# CRITICAL: Force WPA2 (rsn) instead of WPA (wpa)
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.proto rsn',
|
||||
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} ipv4.addresses {AP_IP}/24',
|
||||
|
||||
# Use AES encryption (CCMP) instead of TKIP
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.pairwise ccmp',
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} wifi-sec.group ccmp',
|
||||
|
||||
# Set band
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} 802-11-wireless.band {BAND}',
|
||||
|
||||
# Enable autostart
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} connection.autoconnect yes',
|
||||
f'sudo nmcli connection modify {CONNECTION_NAME} connection.autoconnect-priority 10',
|
||||
|
||||
# Start the AP
|
||||
f'sudo nmcli connection up {CONNECTION_NAME}'
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'net:wifi:access-point',
|
||||
name: 'Configure WiFi Access Point (NetworkManager)',
|
||||
dependencies: () => [],
|
||||
secrets: ['PASSWORD'],
|
||||
schema: z.object({
|
||||
SSID: z.string().min(1).max(32).default('PiPoint').describe('WiFi network name (SSID)'),
|
||||
PASSWORD: z
|
||||
.string()
|
||||
.min(8)
|
||||
.max(63)
|
||||
.default('1223334444')
|
||||
.describe('WPA2 password (8-63 characters)'),
|
||||
BAND: z.enum(['2.4GHz', '5GHz']).default('2.4GHz').describe('Frequency band'),
|
||||
AP_IP: z.string().default('192.168.4.1').describe('AP IP address'),
|
||||
CONNECTION_NAME: z.string().default('pi-point').describe('NetworkManager connection name'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
# network:wifi:connection
|
||||
|
||||
**Configure a WiFi client connection using NetworkManager**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube allows you to connect your target host (e.g., a Raspberry Pi or a laptop) to an existing WiFi network using `nmcli`.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Ensures `network-manager` is installed and running
|
||||
2. Removes any existing connection with the same name to avoid conflicts
|
||||
3. Connects to the specified `SSID` using the provided `PASSWORD`
|
||||
4. Configures the connection to automatically connect on boot (optional)
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Type | Description | Required | Default |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `SSID` | `string` | The WiFi network name | Yes | - |
|
||||
| `PASSWORD` | `string` | The WiFi password | Yes | - |
|
||||
| `AUTOCONNECT` | `boolean` | Automatically connect to this network | No | `true` |
|
||||
| `CONNECTION_NAME` | `string` | Name for the connection in NetworkManager | No | `SSID` |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `apt/essentials`: Basic system utilities.
|
||||
|
||||
## Usage
|
||||
|
||||
### Simple Connection
|
||||
|
||||
```bash
|
||||
nopy install network:wifi:connection --env SSID="MyHomeWiFi" --env PASSWORD="mysecurepassword"
|
||||
```
|
||||
|
||||
### Connection with Custom Name and No Autoconnect
|
||||
|
||||
```bash
|
||||
nopy install network:wifi:connection --env SSID="OfficeWiFi" --env PASSWORD="password123" --env CONNECTION_NAME="Work" --env AUTOCONNECT=false
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- `PASSWORD` is declared in the manifest's `secrets`: nopy keeps it out of session
|
||||
and history files and masks it in every command it prints. It is prompted for
|
||||
again on replay.
|
||||
- That covers what nopy writes, not everything. The value is still on pyinfra's
|
||||
command line, so it is visible in `ps` while the deployment runs.
|
||||
- WiFi passwords will be stored in `/etc/NetworkManager/system-connections/` on the target host.
|
||||
- Passing passwords via `--env` may leave them in your local shell history.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
You can check the status of your WiFi connections on the target host with:
|
||||
```bash
|
||||
nmcli connection show
|
||||
nmcli device status
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import apt, server, systemd
|
||||
|
||||
# [agnt://cogen/cogen/network-wifi-connection-2]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
|
||||
|
||||
"""
|
||||
Deployment script for network:wifi:connection.
|
||||
Uses nmcli to configure a WiFi client connection.
|
||||
"""
|
||||
|
||||
SSID = host.data.SSID
|
||||
PASSWORD = host.data.PASSWORD
|
||||
AUTOCONNECT = "yes" if host.data.get('AUTOCONNECT', True) else "no"
|
||||
CONNECTION_NAME = host.data.get('CONNECTION_NAME', SSID)
|
||||
|
||||
# Install NetworkManager if not present
|
||||
apt.packages(
|
||||
name='Install NetworkManager',
|
||||
packages=['network-manager'],
|
||||
update=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Ensure NetworkManager is running
|
||||
systemd.service(
|
||||
name='Ensure NetworkManager is running',
|
||||
service='NetworkManager',
|
||||
running=True,
|
||||
enabled=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Add/Update the WiFi connection
|
||||
# We delete first to ensure a clean state with the new password/settings
|
||||
server.shell(
|
||||
name=f"Configure WiFi connection for {SSID}",
|
||||
commands=[
|
||||
f'nmcli connection delete "{CONNECTION_NAME}" || true',
|
||||
f'nmcli device wifi connect "{SSID}" password "{PASSWORD}" name "{CONNECTION_NAME}"',
|
||||
f'nmcli connection modify "{CONNECTION_NAME}" connection.autoconnect {AUTOCONNECT}',
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
// [agnt://cogen/cogen/network-wifi-connection-1]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
|
||||
|
||||
/**
|
||||
* Manifest for the network:wifi:connection cube.
|
||||
* Configures a WiFi client connection using NetworkManager (nmcli).
|
||||
*/
|
||||
export default Manifest({
|
||||
id: 'net:wifi:connection',
|
||||
name: 'network:wifi:connection - Connect to a WiFi network',
|
||||
secrets: ['PASSWORD'],
|
||||
schema: z.object({
|
||||
SSID: z.string().min(1).describe('The SSID of the WiFi network to connect to'),
|
||||
PASSWORD: z.string().min(8).describe('The password for the WiFi network'),
|
||||
AUTOCONNECT: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to automatically connect to this network'),
|
||||
CONNECTION_NAME: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional name for the connection (defaults to SSID)'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
# docker
|
||||
|
||||
**Install Docker and tools**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs Docker Engine, Docker Compose, and popular Docker management tools to enable containerized application deployment and management.
|
||||
|
||||
## What is Docker?
|
||||
|
||||
Docker is a platform for developing, shipping, and running applications in containers. Containers package an application with all its dependencies, ensuring it runs consistently across different environments.
|
||||
|
||||
Key benefits:
|
||||
- **Isolation**: Each container runs independently with its own filesystem, network, and processes
|
||||
- **Portability**: Containers run the same way on any system that supports Docker
|
||||
- **Efficiency**: Containers share the host OS kernel, making them lighter than virtual machines
|
||||
- **Scalability**: Easily deploy and scale containerized applications
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Adds Docker's official repository**
|
||||
- Downloads and installs Docker's GPG key
|
||||
- Configures APT to use Docker's official package repository
|
||||
|
||||
2. **Installs Docker components**
|
||||
- `docker-ce` - Docker Community Edition engine
|
||||
- `docker-ce-cli` - Command-line interface for Docker
|
||||
- `containerd.io` - Container runtime
|
||||
- `docker-buildx-plugin` - Extended build capabilities with BuildKit
|
||||
- `docker-compose-plugin` - Tool for defining multi-container applications
|
||||
|
||||
3. **Installs management tools**
|
||||
- **lazydocker** - Terminal UI for Docker and Docker Compose management
|
||||
- **docker-ctop** - Container metrics and monitoring (top-like interface for containers)
|
||||
|
||||
## Configuration
|
||||
|
||||
This cube currently has no configurable parameters.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After deployment:
|
||||
- Add users to the `docker` group to run Docker without sudo: `sudo usermod -aG docker username`
|
||||
- Start using Docker: `docker run hello-world`
|
||||
- Use lazydocker for easy management: `lazydocker`
|
||||
- Monitor containers: `ctop`
|
||||
|
||||
## Notes
|
||||
|
||||
The Docker daemon starts automatically on boot. You can manage it with systemd:
|
||||
- Check status: `sudo systemctl status docker`
|
||||
- Restart: `sudo systemctl restart docker`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Installation tutorial Ubuntu 24.04](https://www.cherryservers.com/blog/install-docker-ubuntu)
|
||||
@@ -0,0 +1,75 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, apt
|
||||
|
||||
|
||||
DISTRO = host.data.DISTRO
|
||||
|
||||
server.shell(
|
||||
commands=[
|
||||
"install -m 0755 -d /etc/apt/keyrings",
|
||||
f"curl -fsSL https://download.docker.com/linux/{DISTRO}/gpg | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg",
|
||||
"sudo chmod a+r /etc/apt/keyrings/docker.gpg",
|
||||
f""" echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/{DISTRO} "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null """,
|
||||
"apt update"
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
packages=[
|
||||
"docker-ce",
|
||||
"docker-ce-cli",
|
||||
"containerd.io",
|
||||
"docker-buildx-plugin",
|
||||
"docker-compose-plugin",
|
||||
],
|
||||
present=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
|
||||
server.shell(
|
||||
name="Install Lazydocker system-wide",
|
||||
commands=[
|
||||
'export DIR=/usr/local/bin && curl -fsSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash -s --'
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Install required packages
|
||||
apt.packages(
|
||||
name="Install required dependencies",
|
||||
packages=["ca-certificates", "curl", "gnupg", "lsb-release"],
|
||||
update=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Download and store the GPG key
|
||||
server.shell(
|
||||
name="Download and store Azlux repo GPG key",
|
||||
commands=[
|
||||
"curl -fsSL https://azlux.fr/repo.gpg.key | gpg --dearmor --yes -o /usr/share/keyrings/azlux-archive-keyring.gpg"
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Add the repository to sources.list
|
||||
server.shell(
|
||||
name="Add Azlux repository to APT sources",
|
||||
commands=[
|
||||
""" echo "deb [arch="$(dpkg --print-architecture)" signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian bookworm main" | sudo tee /etc/apt/sources.list.d/azlux.list > /dev/null """,
|
||||
],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
# Update APT and install docker-ctop
|
||||
apt.update(
|
||||
name="Update package lists",
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name="Install docker-ctop",
|
||||
packages=["docker-ctop"],
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'runtime:docker',
|
||||
name: 'Install docker and tools',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
DISTRO: z.enum(['ubuntu', 'debian']).default('ubuntu').describe('Linux distribution to target'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
# nodevm
|
||||
|
||||
**Install Node.js with essential global packages**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube installs the latest LTS (Long Term Support) version of Node.js along with essential global npm packages commonly needed for development and deployment.
|
||||
|
||||
## What is Node.js?
|
||||
|
||||
Node.js is a JavaScript runtime built on Chrome's V8 engine that allows you to run JavaScript on the server. It's widely used for:
|
||||
|
||||
- Building web servers and APIs
|
||||
- Command-line tools
|
||||
- Build tools and task runners
|
||||
- Real-time applications (chat, notifications)
|
||||
- Microservices
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Installs Node.js LTS**
|
||||
- Downloads and runs the official NodeSource setup script
|
||||
- Installs the latest LTS version of Node.js
|
||||
- Includes npm (Node Package Manager)
|
||||
|
||||
2. **Installs build dependencies**
|
||||
- `libssl-dev` - SSL/TLS libraries
|
||||
- `libtool` - Library building tools
|
||||
- `cmake` - Cross-platform build system
|
||||
- `libpng-dev`, `libjpeg-dev`, `libvips-dev` - Image processing libraries
|
||||
|
||||
3. **Installs global npm packages**
|
||||
- **npm@11.1.0** - Latest npm version
|
||||
- **pm2** - Production process manager for Node.js apps
|
||||
- **yarn** - Alternative package manager
|
||||
- **local-web-server** - Local development web server
|
||||
- **node-gyp** - Node.js native addon build tool
|
||||
- **inquirer** - Interactive command-line prompts
|
||||
- **execa** - Better child process execution
|
||||
- **@dotenvx/dotenvx** - Environment variable management
|
||||
|
||||
## Configuration
|
||||
|
||||
This cube currently has no configurable parameters.
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - this cube can run standalone.
|
||||
|
||||
## Post-Installation
|
||||
|
||||
Verify installation:
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
Common commands:
|
||||
- Run a Node.js app: `node app.js`
|
||||
- Start with PM2: `pm2 start app.js`
|
||||
- Install packages: `npm install <package>`
|
||||
- Use yarn: `yarn add <package>`
|
||||
|
||||
## PM2 - Process Manager
|
||||
|
||||
PM2 is included for production deployments. Common PM2 commands:
|
||||
|
||||
```bash
|
||||
pm2 start app.js # Start application
|
||||
pm2 list # List running apps
|
||||
pm2 stop app # Stop application
|
||||
pm2 restart app # Restart application
|
||||
pm2 logs # View logs
|
||||
pm2 startup # Enable PM2 on boot
|
||||
pm2 save # Save current process list
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Node.js is installed system-wide
|
||||
- Global packages are accessible to all users
|
||||
- npm cache is stored in `~/.npm`
|
||||
- Use `nvm` if you need multiple Node.js versions
|
||||
@@ -0,0 +1,58 @@
|
||||
from pyinfra.operations import server, apt, npm, python
|
||||
from pyinfra import host
|
||||
from pyinfra.facts.files import Directory
|
||||
from pyinfra.facts.server import Which
|
||||
|
||||
hasNode = host.get_fact(Which, 'node')
|
||||
VERSION = host.data.VERSION
|
||||
ALIAS = host.data.ALIAS
|
||||
GLOBAL_PACKAGES = host.data.GLOBAL_PACKAGES
|
||||
USER = host.data.USER
|
||||
|
||||
apt.packages(
|
||||
name=f'Install nodejs tools',
|
||||
no_recommends=True,
|
||||
packages=[
|
||||
'build-essential',
|
||||
'libssl-dev',
|
||||
'libtool',
|
||||
'cmake',
|
||||
'libcairo2-dev',
|
||||
'libpango1.0-de',
|
||||
'libpng-dev',
|
||||
'libgif-dev',
|
||||
'libjpeg-dev',
|
||||
'libvips-dev',
|
||||
'librsvg2-dev',
|
||||
'libpixman-1-dev',
|
||||
],
|
||||
_sudo = True,
|
||||
)
|
||||
|
||||
server.shell(
|
||||
commands=[
|
||||
"curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash",
|
||||
"omf install nvm",
|
||||
f"nvm install {VERSION}",
|
||||
f"nvm alias {ALIAS} {VERSION}",
|
||||
"set -gx NVM_DIR $HOME/.nvm",
|
||||
],
|
||||
_sudo=True,
|
||||
_su_user=USER,
|
||||
_use_su_login=True,
|
||||
_shell_executable='/usr/bin/fish'
|
||||
)
|
||||
|
||||
|
||||
server.shell(
|
||||
commands=[
|
||||
"npm install -g pm2 yarn local-web-server node-gyp inquirer execa @dotenvx/dotenvx"
|
||||
],
|
||||
_sudo=True,
|
||||
_su_user=USER,
|
||||
_use_su_login=True,
|
||||
_shell_executable='/usr/bin/fish'
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'runtime:nodevm',
|
||||
name: 'Install nvm and nodejs with global packages',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
VERSION: z
|
||||
.nullable(z.string())
|
||||
.describe('Node.js version to install. It is recommended to use semver notation')
|
||||
.default('v22.20.0'),
|
||||
USER: z.string().describe('Username for which to install nodejs').default('vagrant'),
|
||||
ALIAS: z.string().describe('The alias for this node version').default('nodelts'),
|
||||
GLOBAL_PACKAGES: z
|
||||
.string()
|
||||
.describe('Space-separated list of global npm packages to install')
|
||||
.default('npm-check-updates'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,176 @@
|
||||
# TypeStack Install Cube
|
||||
|
||||
Deploys a Node.js/TypeScript application from a Git repository as a systemd service with Docker Compose and PM2 support.
|
||||
|
||||
## Features
|
||||
|
||||
- Clones Git repository
|
||||
- Installs dependencies with Yarn
|
||||
- Builds the application
|
||||
- Starts Docker Compose services
|
||||
- Creates a systemd service for automatic startup
|
||||
- Configures PM2 for process management
|
||||
- Automatic restart on failure
|
||||
|
||||
## Requirements
|
||||
|
||||
- Git (for cloning repository)
|
||||
- Yarn (for dependency management)
|
||||
- Docker and Docker Compose
|
||||
- PM2 (for process management)
|
||||
- Node.js/NVM installed
|
||||
- SSH key access to the repository (if using private repos)
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### Required
|
||||
|
||||
- **USER**: System user to run the application (default: `teclabmin`)
|
||||
- **REPO**: Git repository URL (default: `git@github.com:bennidi/teclab-flintstone.git`)
|
||||
- **APP**: Application name/directory name (default: `flintstone`)
|
||||
|
||||
### Optional
|
||||
|
||||
- **ENV**: Application environment (default: `production`)
|
||||
- **AUTOSTART**: Enable and start service immediately (default: `True`)
|
||||
- **NODE_PATH**: Path to Node.js binaries (default: `/home/teclabmin/.nvm/versions/node/v21.7.3/bin`)
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"USER": "myuser",
|
||||
"REPO": "git@github.com:myorg/myapp.git",
|
||||
"APP": "myapp"
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"USER": "appuser",
|
||||
"REPO": "git@github.com:myorg/myapp.git",
|
||||
"APP": "myapp",
|
||||
"ENV": "staging",
|
||||
"AUTOSTART": false,
|
||||
"NODE_PATH": "/home/appuser/.nvm/versions/node/v20.0.0/bin"
|
||||
}
|
||||
```
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Clone Repository**: Clones the specified Git repository to `/home/<USER>/<APP>`
|
||||
2. **Install Dependencies**: Runs `yarn install` to install all dependencies
|
||||
3. **Build Application**: Runs `yarn build` to compile the application
|
||||
4. **Start Docker Services**: Runs `docker compose up -d` to start containerized services
|
||||
5. **Create Startup Script**: Creates `/home/<USER>/<APP>.service.sh` that:
|
||||
- Starts Docker Compose services
|
||||
- Starts PM2 with ecosystem.config.js
|
||||
6. **Create Systemd Service**: Creates `/etc/systemd/system/<APP>.service` that:
|
||||
- Runs after Docker service
|
||||
- Uses the specified user
|
||||
- Configures proper environment (HOME, PATH)
|
||||
- Auto-restarts on failure
|
||||
7. **Enable & Start Service**: Enables and starts the service (if AUTOSTART=True)
|
||||
|
||||
## Service Management
|
||||
|
||||
### Check service status
|
||||
|
||||
```bash
|
||||
sudo systemctl status <APP>
|
||||
```
|
||||
|
||||
### Start the service
|
||||
|
||||
```bash
|
||||
sudo systemctl start <APP>
|
||||
```
|
||||
|
||||
### Stop the service
|
||||
|
||||
```bash
|
||||
sudo systemctl stop <APP>
|
||||
```
|
||||
|
||||
### Restart the service
|
||||
|
||||
```bash
|
||||
sudo systemctl restart <APP>
|
||||
```
|
||||
|
||||
### View service logs
|
||||
|
||||
```bash
|
||||
sudo journalctl -u <APP> -f
|
||||
```
|
||||
|
||||
### Disable autostart
|
||||
|
||||
```bash
|
||||
sudo systemctl disable <APP>
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
After deployment:
|
||||
|
||||
```
|
||||
/home/<USER>/
|
||||
├── <APP>/ # Application directory
|
||||
│ ├── ecosystem.config.js # PM2 configuration
|
||||
│ ├── docker-compose.yml # Docker services
|
||||
│ └── ... # Application files
|
||||
├── <APP>.service.sh # Startup script
|
||||
/etc/systemd/system/
|
||||
└── <APP>.service # Systemd service file
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service fails to start
|
||||
|
||||
1. Check service logs:
|
||||
```bash
|
||||
sudo journalctl -u <APP> -n 50
|
||||
```
|
||||
|
||||
2. Verify Docker is running:
|
||||
```bash
|
||||
sudo systemctl status docker
|
||||
```
|
||||
|
||||
3. Check if Node.js path is correct:
|
||||
```bash
|
||||
which node
|
||||
which pm2
|
||||
```
|
||||
|
||||
### Repository clone fails
|
||||
|
||||
- Ensure SSH keys are properly configured for the user
|
||||
- Test SSH access: `ssh -T git@github.com`
|
||||
- Check repository URL is correct
|
||||
|
||||
### Docker Compose fails
|
||||
|
||||
- Verify Docker is installed and running
|
||||
- Check docker-compose.yml exists in the application directory
|
||||
- Ensure user has Docker permissions: `sudo usermod -aG docker <USER>`
|
||||
|
||||
### PM2 not starting
|
||||
|
||||
- Verify PM2 is installed: `pm2 --version`
|
||||
- Check ecosystem.config.js exists
|
||||
- Verify NODE_PATH includes PM2 binary location
|
||||
|
||||
## Notes
|
||||
|
||||
- The service type is set to `forking` to support PM2's daemon mode
|
||||
- Service will auto-restart on failure with a 5-second delay
|
||||
- Maximum 5 restart attempts in the burst period
|
||||
- The service waits for Docker to be ready before starting
|
||||
- Environment variables can be configured in the ecosystem.config.js file
|
||||
@@ -0,0 +1,27 @@
|
||||
from pyinfra.operations import systemd
|
||||
from pyinfra import host
|
||||
|
||||
|
||||
APP = host.data.APP
|
||||
|
||||
# Enable and start the service based on AUTOSTART flag
|
||||
if AUTOSTART:
|
||||
systemd.service(
|
||||
name=f'Enable {SERVICE_NAME} service',
|
||||
service=APP,
|
||||
enabled=True,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
name=f'Start {SERVICE_NAME} service',
|
||||
service=APP,
|
||||
running=True,
|
||||
_sudo=True
|
||||
)
|
||||
else:
|
||||
server.shell(
|
||||
name=f'Service {SERVICE_NAME} created but not enabled (AUTOSTART=False)',
|
||||
commands=[f'echo "Service {SERVICE_NAME} is ready but not started. Enable with: sudo systemctl enable {APP} && sudo systemctl start {APP}"'],
|
||||
_sudo=False
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'service:autostart',
|
||||
name: 'Manage systemd service autostart',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
APP: z.string().describe('The name of the systemd service (e.g., flintstone)'),
|
||||
SERVICE_NAME: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Display name for the service')
|
||||
.default('Application'),
|
||||
AUTOSTART: z.boolean().describe('Should the service be enabled and started?').default(true),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
# ssh-authorize
|
||||
|
||||
**Authorize SSH public key for a user**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube adds a specific SSH public key to a user's `authorized_keys` file on the remote server, allowing them to log in via SSH using the corresponding private key.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **USER** (string, default: `'vagrant'`)
|
||||
- The username on the remote server to authorize.
|
||||
- If the user does not exist, `pyinfra` will attempt to create it (though a full user creation with shell/groups is better handled by `user-add`).
|
||||
|
||||
- **PUBKEY** (string, required)
|
||||
- The actual content of the public key (e.g., `ssh-ed25519 AAAA...`).
|
||||
|
||||
## Use Cases
|
||||
|
||||
Grant access to a developer:
|
||||
```javascript
|
||||
exec('ssh-authorize', {
|
||||
USER: 'developer',
|
||||
PUBKEY: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...'
|
||||
})
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,13 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server
|
||||
|
||||
USER = host.data.USER
|
||||
PUBKEY = host.data.PUBKEY
|
||||
|
||||
server.user(
|
||||
name=f"Authorize public key for {USER}",
|
||||
user=USER,
|
||||
public_keys=[PUBKEY],
|
||||
present=True,
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'ssh:authorize',
|
||||
name: 'Authorize SSH public key for a user',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
USER: z.string().describe('Username to authorize').default('vagrant'),
|
||||
PUBKEY: z.string().describe('SSH public key string').default(''),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
# ssh-keygen
|
||||
|
||||
**Generate SSH key for a given user**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube generates a new SSH key pair for a specified user, which can be used for secure, passwordless authentication to remote servers and services like GitHub, GitLab, or other SSH-accessible systems.
|
||||
|
||||
## What are SSH Keys?
|
||||
|
||||
SSH keys provide a more secure and convenient way to authenticate compared to passwords:
|
||||
|
||||
- **Public key**: Shared with servers/services you want to access (like GitHub)
|
||||
- **Private key**: Kept secret on your local machine, never shared
|
||||
- **Passphrase-free**: This cube generates keys without a passphrase for automation
|
||||
- **Algorithm support**: RSA (traditional) or Ed25519 (modern, recommended)
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Creates the `.ssh` directory with proper permissions (700)
|
||||
2. Generates an SSH key pair using the specified algorithm
|
||||
3. Saves the keys as `id_{SUFFIX}` and `id_{SUFFIX}.pub`
|
||||
4. Logs the public key to the console for easy copying
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **SUFFIX** (string, default: `'ed25519'`)
|
||||
- Suffix for keyname (e.g., `github` → `id_github.pub`)
|
||||
- Helps identify the purpose of the key
|
||||
|
||||
- **EMAIL** (string, default: `'undefined@bitsquare.dev'`)
|
||||
- Email address to associate with the SSH key
|
||||
- Used as a comment in the public key
|
||||
|
||||
- **ALGORITHM** (enum: `'rsa'` | `'ed25519'`, default: `'ed25519'`)
|
||||
- SSH key algorithm type
|
||||
- **Ed25519**: Modern, faster, more secure (recommended)
|
||||
- **RSA**: Traditional, widely supported
|
||||
|
||||
- **USER** (string, default: `'vagrant'`)
|
||||
- Username for which to generate the SSH key
|
||||
- Inherited from `user-add` dependency
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **user-add** - Creates the user account first
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After the key is generated:
|
||||
|
||||
1. The public key will be logged to the console
|
||||
2. Copy the public key and add it to the target service:
|
||||
- **GitHub**: Settings → SSH and GPG keys → New SSH key
|
||||
- **GitLab**: Preferences → SSH Keys
|
||||
- **Remote server**: Add to `~/.ssh/authorized_keys`
|
||||
|
||||
3. SSH config is automatically set up for the key
|
||||
|
||||
## Key Locations
|
||||
|
||||
- Private key: `/home/{USER}/.ssh/id_{SUFFIX}`
|
||||
- Public key: `/home/{USER}/.ssh/id_{SUFFIX}.pub`
|
||||
|
||||
## Algorithm Comparison
|
||||
|
||||
**Ed25519** (Recommended):
|
||||
- Smaller keys (256-bit)
|
||||
- Faster generation and verification
|
||||
- More secure against certain attacks
|
||||
- Not supported on very old systems
|
||||
|
||||
**RSA**:
|
||||
- Larger keys (2048-4096 bit)
|
||||
- Universally supported
|
||||
- Slower than Ed25519
|
||||
- Well-tested and trusted
|
||||
|
||||
## Example Usage
|
||||
|
||||
Generate a key for GitHub access:
|
||||
```javascript
|
||||
exec('ssh-keygen', {
|
||||
SUFFIX: 'github',
|
||||
EMAIL: 'myemail@example.com',
|
||||
ALGORITHM: 'ed25519',
|
||||
USER: 'myuser'
|
||||
})
|
||||
```
|
||||
|
||||
This creates `id_github` and `id_github.pub` in `/home/myuser/.ssh/`.
|
||||
@@ -0,0 +1,46 @@
|
||||
from pyinfra.operations import files, server, python
|
||||
from pyinfra import host, logger
|
||||
import logging
|
||||
|
||||
|
||||
NAME=host.data.SUFFIX
|
||||
EMAIL=host.data.EMAIL
|
||||
ALGORITHM=host.data.ALGORITHM
|
||||
USER=host.data.USER
|
||||
|
||||
# Ensure the .ssh directory exists
|
||||
files.directory(
|
||||
name="Ensure .ssh directory exists",
|
||||
path=f"/home/{USER}/.ssh",
|
||||
present=True,
|
||||
mode=700,
|
||||
user=USER,
|
||||
group=USER,
|
||||
)
|
||||
|
||||
# Generate the SSH keypair
|
||||
server.shell(
|
||||
name=f"Generate SSH key id_{NAME}",
|
||||
commands=[
|
||||
f"ssh-keygen -t {ALGORITHM} -f /home/{USER}/.ssh/id_{NAME} -C '{EMAIL}' -N ''"
|
||||
]
|
||||
)
|
||||
|
||||
# Print the public key
|
||||
result = server.shell(
|
||||
name="Print public key",
|
||||
commands=[f"cat /home/{USER}/.ssh/id_{NAME}.pub"],
|
||||
)
|
||||
|
||||
def callback():
|
||||
# 🔹 Extract and log the key output
|
||||
if result.stdout:
|
||||
logger.info(f"Public Key for {USER}: {result.stdout.strip()}")
|
||||
else:
|
||||
logger.warning(f"No public key found for {USER} at /home/{USER}/.ssh/id_{NAME}.pub")
|
||||
|
||||
python.call(
|
||||
name="Log public key",
|
||||
function=callback,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'ssh:keygen',
|
||||
name: 'Generate SSH key for a given $USER',
|
||||
dependencies: () => ['user:add'],
|
||||
schema: z.object({
|
||||
SUFFIX: z
|
||||
.string()
|
||||
.describe('Suffix for keyname, e.g. github => id_github.pub')
|
||||
.default('ed25519'),
|
||||
EMAIL: z
|
||||
.string()
|
||||
.describe('Email address to associate with the SSH key')
|
||||
.default('undefined@bitsquare.dev'),
|
||||
ALGORITHM: z.enum(['rsa', 'ed25519']).describe('SSH key algorithm type').default('ed25519'),
|
||||
USER: z.string().describe('Username for which to generate the SSH key').default('vagrant'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
# ssh-keyman
|
||||
|
||||
**Deploy existing SSH keys to host**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube deploys pre-existing SSH key pairs from your local machine to a remote server, configuring them for automatic use with specified hosts (like GitHub, GitLab, etc.).
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Copies SSH keys to the server**
|
||||
- Transfers both private and public keys from local directory to remote `.ssh` folder
|
||||
- Sets correct file permissions (600 for private, 644 for public)
|
||||
|
||||
2. **Configures SSH client**
|
||||
- Creates/updates `.ssh/config` to use the deployed key for specified hosts
|
||||
- Disables strict host key checking for easier automation
|
||||
- Maps each host to use the correct identity file
|
||||
|
||||
3. **Ensures security**
|
||||
- Sets proper directory permissions (700 for `.ssh`)
|
||||
- Ensures keys are owned by the specified user
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **KEY_NAME** (string, default: `'id_ed25519'`)
|
||||
- Name of the SSH key file (without extension)
|
||||
- Must exist in the KEY_DIR directory locally
|
||||
|
||||
- **USER** (string, default: `'vagrant'`)
|
||||
- Username for which to deploy the SSH key
|
||||
- Inherited from `user-add` dependency
|
||||
|
||||
- **HOSTS** (string, default: `'github.com'`)
|
||||
- Space-separated list of hosts to add to known_hosts
|
||||
- Example: `'github.com gitlab.com bitbucket.org'`
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **user-add** - Creates the user account first
|
||||
|
||||
## Use Cases
|
||||
|
||||
Deploy GitHub SSH key:
|
||||
|
||||
```javascript
|
||||
exec('ssh-keyman', {
|
||||
KEY_NAME: 'id_github',
|
||||
HOSTS: 'github.com'
|
||||
})
|
||||
```
|
||||
|
||||
Deploy key for multiple Git services:
|
||||
|
||||
```javascript
|
||||
exec('ssh-keyman', {
|
||||
KEY_NAME: 'id_git',
|
||||
HOSTS: 'github.com gitlab.com bitbucket.org'
|
||||
})
|
||||
```
|
||||
|
||||
## What Gets Configured
|
||||
|
||||
After deployment, the `.ssh/config` file will contain entries like:
|
||||
|
||||
```
|
||||
Host github.com
|
||||
IdentityFile /home/{USER}/.ssh/id_github
|
||||
StrictHostKeyChecking no
|
||||
```
|
||||
|
||||
This means when you run `git clone git@github.com:user/repo.git`, it will automatically use the deployed key.
|
||||
|
||||
## Key File Requirements
|
||||
|
||||
The local KEY_DIR must contain:
|
||||
|
||||
- `{KEY_NAME}` - Private key file
|
||||
- `{KEY_NAME}.pub` - Public key file
|
||||
|
||||
For example, if `KEY_NAME=id_github`, you need:
|
||||
|
||||
- `./vault/tmp/id_github`
|
||||
- `./vault/tmp/id_github.pub`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Private keys are sensitive**: Ensure your local KEY_DIR is secure
|
||||
- **StrictHostKeyChecking disabled**: Convenient but less secure
|
||||
- Consider enabling it for production: Edit `/home/{USER}/.ssh/config`
|
||||
- **Backup your keys**: Keep secure copies of private keys
|
||||
- **Use different keys**: Consider separate keys for different services
|
||||
|
||||
## Post-Installation
|
||||
|
||||
Test SSH connection:
|
||||
|
||||
```bash
|
||||
ssh -T git@github.com
|
||||
# Should show: "Hi username! You've successfully authenticated..."
|
||||
```
|
||||
|
||||
Clone a repository:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:user/repo.git
|
||||
# Should work without prompting for credentials
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
from pyinfra.operations import server, files, apt, systemd
|
||||
from pyinfra import host
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
|
||||
def get_keyman_config():
|
||||
"""Get keyman configuration by calling keyman --print-config"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['keyman', '--print-config'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
# Load keyman config for defaults
|
||||
keyman_config = get_keyman_config()
|
||||
|
||||
USER = host.data.USER
|
||||
KEY = host.data.KEY_NAME
|
||||
|
||||
# Use KEY_DIR from host data, or fall back to keyman config tmpDir
|
||||
DIR = host.data.get('KEY_DIR')
|
||||
if not DIR and keyman_config:
|
||||
DIR = keyman_config.get('tmpDir')
|
||||
if not DIR:
|
||||
DIR = '../../vault/tmp' # Final fallback
|
||||
|
||||
# Support multiple hosts separated by space
|
||||
HOSTS = map(str.lstrip, str(host.data.HOSTS).split(' '))
|
||||
|
||||
# 🔹 Define remote paths
|
||||
SSH_DIR = f"/home/{USER}/.ssh" if USER != "root" else "/root/.ssh"
|
||||
PRIVATE_KEY_PATH = f"{SSH_DIR}/{KEY}"
|
||||
PUBLIC_KEY_PATH = f"{SSH_DIR}/{KEY}.pub"
|
||||
|
||||
# Ensure the .ssh directory exists
|
||||
files.directory(
|
||||
name="Ensure .ssh directory exists",
|
||||
path=SSH_DIR,
|
||||
present=True,
|
||||
mode=700,
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True
|
||||
)
|
||||
# Copy private key to the remote server
|
||||
files.put(
|
||||
name="Copy private key",
|
||||
src=f"{DIR}/{KEY}",
|
||||
dest=PRIVATE_KEY_PATH,
|
||||
mode="600",
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
# Copy public key to the remote server
|
||||
files.put(
|
||||
name="Copy public key",
|
||||
src=f"{DIR}/{KEY}.pub",
|
||||
dest=PUBLIC_KEY_PATH,
|
||||
mode="644",
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
# Ensure correct permissions for the private key
|
||||
server.shell(
|
||||
name="Set correct permissions for private key",
|
||||
commands=[f"chmod 600 {PRIVATE_KEY_PATH}"],
|
||||
_sudo=True,
|
||||
)
|
||||
|
||||
files.file(
|
||||
name="Ensure .ssh/config directory exists",
|
||||
path=f"/home/{USER}/.ssh/config",
|
||||
present=True,
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
for host in HOSTS:
|
||||
files.line(
|
||||
name=f"Configure SSH key for {host}",
|
||||
path=f"{SSH_DIR}/config",
|
||||
line=f"Host {host}\n IdentityFile {SSH_DIR}/{KEY}\n StrictHostKeyChecking no",
|
||||
_sudo=True
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'ssh:keyman',
|
||||
name: 'Deploy an ssh key managed by keyman',
|
||||
dependencies: () => [],
|
||||
schema: z.object({
|
||||
KEY_NAME: z
|
||||
.string()
|
||||
.describe('Name of the SSH key file (without extension)')
|
||||
.default('id_ed25519'),
|
||||
USER: z.string().describe('Username for which to deploy the SSH key').default('vagrant'),
|
||||
HOSTS: z
|
||||
.string()
|
||||
.describe('Space-separated list of hosts to add to known_hosts')
|
||||
.default('github.com'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
# user-add
|
||||
|
||||
**Add a user with Fish shell and tools**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube creates a new user account with a modern shell environment (Fish), SSH key authentication, and enhanced productivity tools pre-configured.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. **Creates a new user account**
|
||||
- Sets up home directory with proper permissions
|
||||
- Configures password authentication
|
||||
- Adds user to specified groups (e.g., `docker`, `sudo`)
|
||||
- Sets Fish as the default login shell
|
||||
|
||||
2. **Configures SSH access**
|
||||
- Deploys the specified SSH public key for passwordless authentication
|
||||
- Creates `.ssh` directory with proper permissions
|
||||
- Sets up SSH config file
|
||||
- Configures SSH agent auto-loading for Fish shell
|
||||
|
||||
3. **Installs Fish shell enhancements**
|
||||
- Installs **Oh My Fish** (OMF) - Fish shell framework with themes and plugins
|
||||
- Deploys custom Fish configuration (`config.fish`)
|
||||
- Sets up Fish rc directory for modular configurations
|
||||
|
||||
4. **Creates workspace directories**
|
||||
- Creates `/home/{USER}/tmp` directory for temporary files
|
||||
|
||||
## Configuration
|
||||
|
||||
### Parameters
|
||||
|
||||
- **USER** (string, auto-generated)
|
||||
- Username for the new user account
|
||||
- Default: `userXXXXX` (randomly generated 5-character suffix)
|
||||
|
||||
- **PASSWORD** (string, **secret**)
|
||||
- Password for the new user account
|
||||
- Default: the literal `changeme` — a placeholder, not a credential. Change it
|
||||
on first login, or pass a real one.
|
||||
- Declared in the manifest's `secrets`, so it is never written to a session or
|
||||
history file and is masked in printed commands. A replay asks for it again.
|
||||
- It used to default to a randomly generated password. That was removed: since
|
||||
the value is not recorded, an unattended run created an account with a
|
||||
credential nobody had seen, and replaying that run produced a different one.
|
||||
|
||||
- **GROUPS** (string, default: `''`)
|
||||
- Comma-separated list of additional groups (e.g., `"docker,sudo"`)
|
||||
- Common groups:
|
||||
- `docker` - Run Docker without sudo
|
||||
- `sudo` - Administrative privileges
|
||||
- `www-data` - Web server file access
|
||||
|
||||
- **PUBKEY** (string, **required** — no default)
|
||||
- SSH public key to authorize for the user
|
||||
- Should be your public key for passwordless SSH access
|
||||
- There is deliberately no default. It used to be a specific personal key, so
|
||||
accepting the default authorized *someone else's* key on the new account.
|
||||
No key would be a sensible guess, so the cube asks instead.
|
||||
- Because it is required, `--use-defaults` refuses to run this cube unless
|
||||
`PUBKEY` comes from `env` in `.nopyrc.json`, a dependency, or a hook.
|
||||
- Submitting an empty value at the prompt authorizes no key at all (the account
|
||||
is still created, with password login only).
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **apt:essentials** - Provides Fish shell and basic tools
|
||||
|
||||
## What is Fish?
|
||||
|
||||
Fish (Friendly Interactive Shell) is a modern command-line shell that focuses on usability:
|
||||
|
||||
- **Smart autosuggestions**: Suggests commands as you type based on history
|
||||
- **Syntax highlighting**: Color-codes commands in real-time
|
||||
- **Tab completions**: Comprehensive, discoverable command completions
|
||||
- **No configuration needed**: Works great out of the box
|
||||
|
||||
## Post-Installation
|
||||
|
||||
After deployment:
|
||||
- SSH into the server as the new user: `ssh {USER}@server`
|
||||
- Your SSH key will be pre-authorized (no password needed if using key)
|
||||
- Fish shell will start automatically with OMF installed
|
||||
- SSH agent auto-loads to manage your SSH keys
|
||||
|
||||
## Notes
|
||||
|
||||
- The user's home directory is created at `/home/{USER}`
|
||||
- Fish configuration is stored in `/home/{USER}/.config/fish/`
|
||||
- Oh My Fish provides package management: `omf install <package>`
|
||||
- To switch shells: `chsh -s /bin/bash` (or back to fish: `chsh -s /usr/bin/fish`)
|
||||
|
||||
---
|
||||
|
||||
# 📌 Most Useful Fish Key Bindings (with Fisher Extensions)
|
||||
|
||||
## 🐟 Default Fish Key Bindings
|
||||
|
||||
- `Ctrl + C` → Cancel the current command
|
||||
- `Ctrl + D` → Exit the shell (or logout if in SSH)
|
||||
- `Ctrl + L` → Clear the terminal
|
||||
- `Ctrl + R` → Search command history (enhanced by `fzf.fish`)
|
||||
- `Ctrl + U` → Delete the entire command line
|
||||
- `Ctrl + W` → Delete the last word
|
||||
- `Alt + ← / →` → Move backward/forward by a word
|
||||
|
||||
## 🔍 Enhanced with `fzf.fish`
|
||||
|
||||
- `Ctrl + R` → **Fuzzy search command history**
|
||||
- `Ctrl + T` → **Fuzzy search and insert file path**
|
||||
- `Alt + C` → **Fuzzy search directories (`cd` with `z`)**
|
||||
|
||||
## 📂 Directory Navigation (with `z`)
|
||||
|
||||
- `z <dir>` → Jump to a frequently used directory
|
||||
- `z -l` → List most-used directories
|
||||
- `z -c` → Remove a directory from `z`'s database
|
||||
|
||||
## 🔄 Process & Job Management
|
||||
|
||||
- `Ctrl + Z` → Suspend the current process
|
||||
- `fg` → Bring a suspended process back to foreground
|
||||
- `jobs` → List background jobs
|
||||
|
||||
## 🎨 Other Handy Shortcuts
|
||||
|
||||
- `fish_vi_key_bindings` → Enable Vi mode (press `Esc` for normal mode)
|
||||
- `Ctrl + G` → Show Git status (if using `fzf.fish`)
|
||||
- `Ctrl + E` → Edit command line in `$EDITOR`
|
||||
|
||||
## ⚙️ Useful Commands for Key Binding
|
||||
|
||||
```fish
|
||||
# Set Fish default key bindings
|
||||
fish_default_key_bindings
|
||||
|
||||
# Enable Vi mode
|
||||
fish_vi_key_bindings
|
||||
|
||||
# Rebind a custom key (Example: Ctrl + G for git status)
|
||||
bind \cg 'git status'
|
||||
@@ -0,0 +1,10 @@
|
||||
if status is-interactive
|
||||
# Commands to run in interactive sessions can go here
|
||||
# Execute all scripts in ~/.config/fish/rc/ on shell startup
|
||||
for script in ~/.config/fish/rc/*.fish
|
||||
if test -f $script
|
||||
source $script
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server, files, apt
|
||||
from io import StringIO
|
||||
|
||||
# Define the username, password, and public key for the new admin user
|
||||
USER = host.data.USER
|
||||
HOME_DIR = f"/home/{USER}"
|
||||
TMP_DIR = f"{HOME_DIR}/tmp"
|
||||
PASSWORD = host.data.PASSWORD
|
||||
# An empty submission at the prompt must not become an empty authorized_keys
|
||||
# line, so an absent key means no key rather than a blank one.
|
||||
PUBKEY = host.data.PUBKEY
|
||||
PUBKEYS = [PUBKEY] if PUBKEY and str(PUBKEY).strip() else []
|
||||
GROUPS = list(filter(None, map(str.strip, str(host.data.GROUPS).split())))
|
||||
FISH_PATH = "/usr/bin/fish"
|
||||
FISH_CONFIG_DIR = f"{HOME_DIR}/.config/fish"
|
||||
FISH_CONFIG_FILE = f"{FISH_CONFIG_DIR}/config.fish"
|
||||
FISH_RC_DIR = f"{FISH_CONFIG_DIR}/rc"
|
||||
SSH_AGENT_SCRIPT = f"{FISH_RC_DIR}/ssh-agent.fish"
|
||||
|
||||
apt.packages(
|
||||
name='Ensure fish shell is installed',
|
||||
packages=[ 'fish'],
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
|
||||
# Ensure the user exists with a login shell
|
||||
server.user(
|
||||
name=f"Create user {USER} [{GROUPS}]",
|
||||
present=True,
|
||||
user=USER,
|
||||
password=PASSWORD,
|
||||
create_home=True,
|
||||
groups=GROUPS,
|
||||
shell=FISH_PATH,
|
||||
public_keys=PUBKEYS,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
for dir in [f"{HOME_DIR}/.ssh", FISH_RC_DIR, TMP_DIR]:
|
||||
files.directory(
|
||||
name=f"Ensure {dir} directory exists",
|
||||
path=dir,
|
||||
present=True,
|
||||
mode=700,
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True,
|
||||
_sudo_user=USER,
|
||||
_use_sudo_login=True
|
||||
)
|
||||
|
||||
files.file(
|
||||
name="Ensure .ssh/config exists",
|
||||
path=f"{HOME_DIR}/.ssh/config",
|
||||
present=True,
|
||||
user=USER,
|
||||
group=USER,
|
||||
_sudo=True
|
||||
)
|
||||
|
||||
server.shell(
|
||||
name=f"Install OMF(Oh My Fish) for {USER}",
|
||||
commands=[
|
||||
f"curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install > install-omf",
|
||||
f"fish install-omf --yes --noninteractive",
|
||||
],
|
||||
_sudo=True,
|
||||
_sudo_user=USER,
|
||||
_use_sudo_login=True
|
||||
)
|
||||
|
||||
files.put(
|
||||
name="Add SSH agent auto-load script to Fish rc directory",
|
||||
src="ssh-agent.fish",
|
||||
dest=SSH_AGENT_SCRIPT,
|
||||
user=USER,
|
||||
group=USER,
|
||||
mode="755", # Make it executable
|
||||
_sudo=True,
|
||||
|
||||
)
|
||||
|
||||
files.put(
|
||||
name="Add custom config.fish",
|
||||
src="config.fish",
|
||||
dest=FISH_CONFIG_FILE,
|
||||
user=USER,
|
||||
group=USER,
|
||||
mode="755", # Make it executable
|
||||
_sudo=True,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Manifest, uniqid } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default Manifest({
|
||||
id: 'user:add',
|
||||
name: 'Add a user with fish shell and tools',
|
||||
dependencies: () => ['apt:essentials'],
|
||||
secrets: ['PASSWORD'],
|
||||
schema: z.object({
|
||||
USER: z
|
||||
.string()
|
||||
.describe('Username for the new user account')
|
||||
.default(() => `user${uniqid(5)}`),
|
||||
// A fixed placeholder, not a generated one: the password is never recorded
|
||||
// in a session, so a generated default meant every run produced credentials
|
||||
// nobody had seen and a replay produced different ones again.
|
||||
PASSWORD: z.string().describe('Password for the new user account').default('changeme'),
|
||||
GROUPS: z
|
||||
.string()
|
||||
.describe('Comma-separated list of additional groups (e.g., "docker,sudo")')
|
||||
.default(''),
|
||||
// No default on purpose. This used to carry a specific personal key, which
|
||||
// meant an unattended run authorised someone else's key on the new account.
|
||||
// Leaving it required makes `--use-defaults` refuse by name instead of
|
||||
// guessing, and there is no key that would be a sensible guess.
|
||||
PUBKEY: z.string().describe('SSH public key to authorize for the user'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
# Start SSH agent if not already running
|
||||
if not set -q SSH_AUTH_SOCK
|
||||
eval (ssh-agent -c)
|
||||
end
|
||||
|
||||
# Add all private SSH keys in ~/.ssh to the agent
|
||||
for key in ~/.ssh/id_*;
|
||||
if test -f $key; and not string match -q "*pub" $key
|
||||
ssh-add $key 2>/dev/null
|
||||
end
|
||||
end
|
||||
# Export user and group ID for Docker
|
||||
set -x UID (id -u)
|
||||
set -x GID (id -g)
|
||||
@@ -0,0 +1,52 @@
|
||||
# user:edit
|
||||
|
||||
**Modify an existing user's password or group membership**
|
||||
|
||||
## Purpose
|
||||
|
||||
This cube allows you to update existing user accounts on the target system. It can be used to change passwords, add users to new groups (like `docker` or `sudo`), or revoke group memberships.
|
||||
|
||||
## What This Cube Does
|
||||
|
||||
1. Identifies the existing user on the target system
|
||||
2. Updates the user's password if `PASSWORD` is provided
|
||||
3. Adds the user to the groups specified in `GROUPS`
|
||||
4. Removes the user from the groups specified in `GROUPS_ABSENT`
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Type | Description | Required |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `USER` | `string` | The username of the account to modify | Yes |
|
||||
| `PASSWORD` | `string` | New password for the user. **Secret**: never recorded in a session or history file, masked in printed commands, re-prompted on replay. | No |
|
||||
| `GROUPS` | `string` | Comma-separated list of groups to ADD (e.g., `docker,sudo`) | No |
|
||||
| `GROUPS_ABSENT` | `string` | Comma-separated list of groups to REMOVE | No |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `apt/essentials`: Standard system utilities.
|
||||
|
||||
## Usage
|
||||
|
||||
### Changing a Password
|
||||
|
||||
```bash
|
||||
nopy install user:edit --env USER=myuser --env PASSWORD=newsecurepassword
|
||||
```
|
||||
|
||||
### Adding a User to the Docker Group
|
||||
|
||||
```bash
|
||||
nopy install user:edit --env USER=myuser --env GROUPS=docker
|
||||
```
|
||||
|
||||
### Revoking Sudo Access
|
||||
|
||||
```bash
|
||||
nopy install user:edit --env USER=myuser --env GROUPS_ABSENT=sudo
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- When setting passwords via the CLI, they may be visible in your shell history. Consider using a session file or interactive prompts for sensitive values.
|
||||
- Changing your own user's groups or password may require a re-login to take full effect.
|
||||
@@ -0,0 +1,24 @@
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import server
|
||||
|
||||
# [agnt://cogen/cogen/user-edit-2]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
|
||||
|
||||
"""
|
||||
Deployment script for user:edit.
|
||||
Updates password and group membership for an existing user.
|
||||
"""
|
||||
|
||||
USER = host.data.USER
|
||||
PASSWORD = host.data.get('PASSWORD')
|
||||
GROUPS = [g.strip() for g in str(host.data.get('GROUPS', '')).split(',') if g.strip()]
|
||||
GROUPS_ABSENT = [g.strip() for g in str(host.data.get('GROUPS_ABSENT', '')).split(',') if g.strip()]
|
||||
|
||||
# Update user details
|
||||
server.user(
|
||||
name=f"Update user {USER}",
|
||||
user=USER,
|
||||
password=PASSWORD,
|
||||
groups=GROUPS,
|
||||
groups_absent=GROUPS_ABSENT,
|
||||
_sudo=True,
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Manifest } from '@bitsquare/nopy-cubes';
|
||||
import { z } from 'zod';
|
||||
|
||||
// [agnt://cogen/cogen/user-edit-1]{cartridge: "ansiblings/cubes", action: "generated", status: "generated"}
|
||||
|
||||
/**
|
||||
* Manifest for the user:edit cube.
|
||||
* Allows modifying existing user accounts (password, groups).
|
||||
*/
|
||||
export default Manifest({
|
||||
id: 'user:edit',
|
||||
name: 'user:edit - Modify an existing user account',
|
||||
dependencies: () => [],
|
||||
secrets: ['PASSWORD'],
|
||||
schema: z.object({
|
||||
USER: z.string().describe('The username of the account to modify'),
|
||||
PASSWORD: z.string().optional().describe('New password for the user (optional)'),
|
||||
GROUPS: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Comma-separated list of groups the user SHOULD be in (optional)'),
|
||||
GROUPS_ABSENT: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Comma-separated list of groups to REMOVE from the user (optional)'),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@bitsquare/nopy-cubes-core",
|
||||
"version": "0.5.0",
|
||||
"description": "The core nopy cube bundle: apt, users, ssh, networking, services and runtimes.",
|
||||
"keywords": [
|
||||
"nopy",
|
||||
"nopy-cubess",
|
||||
"pyinfra",
|
||||
"deployment",
|
||||
"infrastructure"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "bitsquare",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://gitea.bitsquare.dev/BitSquare/ansiblings.git",
|
||||
"directory": "packages/nopy-cubes-core"
|
||||
},
|
||||
"homepage": "https://gitea.bitsquare.dev/BitSquare/ansiblings/src/branch/main/packages/nopy-cubes-core",
|
||||
"bugs": {
|
||||
"url": "https://gitea.bitsquare.dev/BitSquare/ansiblings/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"nopy": {
|
||||
"cubes": [
|
||||
"./cubes"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"cubes",
|
||||
"!cubes/**/*.log",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bitsquare/nopy-cubes": "workspace:*",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user