···11+# OS files
22+.DS_Store
33+Thumbs.db
44+55+# IDE
66+.vscode/
77+.idea/
88+99+# Logs
1010+*.log
1111+1212+# Docker data (contains secrets)
1313+services/*/data/
1414+*/data/
1515+1616+# Temporary files
1717+/tmp/
1818+*.tmp
1919+*.swp
2020+*~
2121+2222+# Service-specific ignores
2323+# LLDAP
2424+services/lldap/data/
2525+lldap_data/
2626+2727+# Test files
2828+test-*
2929+*-test.*
+90
CLAUDE.md
···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44+55+## Project Overview
66+77+tinsnip provides shared infrastructure services for homelab environments:
88+- Runs all services under a dedicated `tinsnip` user (UID 1010)
99+- Uses rootless Docker for container isolation
1010+- Currently provides LLDAP for identity management
1111+- Designed to add Redis and Prometheus in the future
1212+1313+## Project Structure
1414+1515+```
1616+tinsnip/
1717+├── install.sh # Downloads and installs tinsnip
1818+├── setup.sh # Main setup orchestrator
1919+├── scripts/
2020+│ ├── create_tinsnip_user.sh # Creates tinsnip user and system users
2121+│ ├── setup_rootless_docker.sh # Installs rootless Docker for tinsnip
2222+│ └── deploy_service.sh # Generic service deployment script
2323+└── services/
2424+ └── lldap/ # LLDAP identity service
2525+ ├── docker-compose.yml # Container configuration
2626+ └── setup.sh # LLDAP-specific setup
2727+```
2828+2929+## Key Commands
3030+3131+### Install tinsnip
3232+```bash
3333+curl -fsSL "https://tangled.sh/dynamicalsystem.com/tinsnip/raw/main/install.sh?$(date +%s)" | bash
3434+cd ~/.local/opt/tinsnip
3535+./setup.sh
3636+```
3737+3838+### Manage services
3939+```bash
4040+# Switch to tinsnip user
4141+sudo -u tinsnip -i
4242+4343+# Check service status
4444+cd ~/services/lldap
4545+docker compose ps
4646+4747+# View logs
4848+docker compose logs -f
4949+```
5050+5151+### Add new service
5252+1. Create `services/<service-name>/` directory
5353+2. Add `docker-compose.yml` for the service
5454+3. Optional: Add `setup.sh` for service-specific setup
5555+4. Deploy with: `./scripts/deploy_service.sh <service-name>`
5656+5757+## Architecture Notes
5858+5959+1. **User Separation**:
6060+ - `tinsnip` (UID 1010) - Regular user that runs rootless Docker
6161+ - `lldap` (UID 999) - System user for LLDAP container
6262+ - Services run isolated from regular system users
6363+6464+2. **Service Pattern**:
6565+ - Each service has its own directory under `services/`
6666+ - All services run under tinsnip's rootless Docker
6767+ - Systemd services created for auto-start
6868+6969+3. **Network**:
7070+ - Services share the `tinsnip_network` Docker network
7171+ - Ports are exposed on all interfaces for homelab access
7272+7373+4. **Storage**:
7474+ - Service data stored in `./data` relative to service directory
7575+ - Owned by appropriate system user (e.g., 999 for LLDAP)
7676+7777+## Security Considerations
7878+7979+- Never run setup.sh as root or as the tinsnip user
8080+- The tinsnip user has no sudo access
8181+- Each service runs as a system user inside containers
8282+- Secrets are generated per-service and stored with restrictive permissions
8383+8484+## Adding New Services
8585+8686+When adding Redis or Prometheus:
8787+1. Follow the existing LLDAP pattern
8888+2. Create appropriate system users if needed
8989+3. Use the shared tinsnip_network
9090+4. Document the ports and configuration
+72
README.md
···11+# tinsnip - Homelab Infrastructure Services
22+33+Shared infrastructure services for homelab environments, deployed with proper isolation and security.
44+55+## Overview
66+77+tinsnip provides essential infrastructure services that multiple homelab systems can share:
88+- **Identity Management** (LLDAP)
99+- More services coming soon...
1010+1111+## Architecture
1212+1313+**Host Level:**
1414+- User: `tinsnip` (UID 1010) - Runs rootless Docker
1515+- Manages all tinsnip service containers
1616+- Complete isolation from regular user accounts
1717+1818+**Service: LLDAP**
1919+- Container runs under tinsnip's rootless Docker
2020+- Inside container: runs as system user `lldap` (UID 999)
2121+- Ports: 3890 (LDAP), 17170 (Web UI)
2222+- Base DN: `dc=home,dc=local`
2323+2424+## Quick Start
2525+2626+```bash
2727+# Install tinsnip
2828+curl -fsSL "https://tangled.sh/dynamicalsystem.com/tinsnip/raw/main/install.sh?$(date +%s)" | bash
2929+3030+# Or clone and run
3131+git clone git@tangled.sh:dynamicalsystem.com/tinsnip
3232+cd tinsnip
3333+./setup.sh
3434+```
3535+3636+## Service Isolation Model
3737+3838+```
3939+User 'tinsnip' (1010) → Runs rootless Docker → Service containers
4040+ ↓ ↓
4141+Isolated from regular users Each runs as appropriate user
4242+```
4343+4444+## Integration Guide
4545+4646+### For Ubuntu/Debian Systems
4747+```bash
4848+# Configure LDAP client
4949+apt install sssd-ldap
5050+# Point to tinsnip:3890
5151+```
5252+5353+### For Docker Services
5454+```yaml
5555+environment:
5656+ - LDAP_HOST=tinsnip
5757+ - LDAP_PORT=3890
5858+ - LDAP_BASE_DN=dc=home,dc=local
5959+```
6060+6161+## Planned Services
6262+6363+- [x] LLDAP - Identity Management
6464+- [ ] Redis - Caching/queuing
6565+- [ ] Prometheus - Metrics collection
6666+6767+## Design Principles
6868+6969+1. **Service Isolation** - All services run under dedicated `tinsnip` user
7070+2. **Rootless Docker** - No root daemon required
7171+3. **Easy Integration** - Standard protocols and ports
7272+4. **Homelab Focused** - Optimized for home infrastructure
+95
install.sh
···11+#!/bin/bash
22+33+set -euo pipefail
44+55+# tinsnip installer - Downloads and sets up tinsnip infrastructure services
66+77+REPO_URL="https://tangled.sh/dynamicalsystem.com/tinsnip"
88+BRANCH="main"
99+INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/opt/tinsnip}"
1010+USE_GIT="${USE_GIT:-false}"
1111+1212+log() {
1313+ echo "[Installer] $*"
1414+}
1515+1616+error() {
1717+ log "ERROR: $*" >&2
1818+ exit 1
1919+}
2020+2121+download_file() {
2222+ local file_path="$1"
2323+ local dest_path="$2"
2424+ local url="${REPO_URL}/raw/${BRANCH}/${file_path}"
2525+2626+ log "Downloading $file_path..."
2727+2828+ if command -v curl &> /dev/null; then
2929+ if curl -fsSL "$url" -o "$dest_path" 2>/dev/null; then
3030+ chmod +x "$dest_path" 2>/dev/null || true
3131+ return 0
3232+ fi
3333+ fi
3434+3535+ error "Failed to download $file_path"
3636+}
3737+3838+clone_with_git() {
3939+ log "Cloning repository with git..."
4040+ if ! command -v git &> /dev/null; then
4141+ log "Git not found, installing..."
4242+ sudo apt-get update -qq && sudo apt-get install -y git
4343+ fi
4444+4545+ if ! git clone "git@tangled.sh:dynamicalsystem.com/tinsnip" "$INSTALL_DIR"; then
4646+ error "Failed to clone repository. Make sure you have SSH access to git@tangled.sh"
4747+ fi
4848+}
4949+5050+main() {
5151+ log "tinsnip Infrastructure Installer"
5252+ log "================================"
5353+5454+ cd ~ || error "Failed to change to home directory"
5555+5656+ if [[ ! -f /etc/os-release ]] || ! grep -q "Ubuntu" /etc/os-release; then
5757+ error "This installer requires Ubuntu"
5858+ fi
5959+6060+ if [[ -d "$INSTALL_DIR" ]]; then
6161+ log "Directory $INSTALL_DIR already exists. Removing for fresh installation..."
6262+ rm -rf "$INSTALL_DIR"
6363+ fi
6464+6565+ if [[ "$USE_GIT" == "true" ]]; then
6666+ mkdir -p "$(dirname "$INSTALL_DIR")"
6767+ clone_with_git
6868+ else
6969+ log "Creating installation directory at $INSTALL_DIR..."
7070+ mkdir -p "$INSTALL_DIR"/{scripts,services/lldap,config}
7171+7272+ log "Downloading setup files..."
7373+7474+ # Download main files
7575+ download_file "setup.sh" "$INSTALL_DIR/setup.sh"
7676+ download_file "README.md" "$INSTALL_DIR/README.md"
7777+7878+ # Download scripts
7979+ download_file "scripts/create_tinsnip_user.sh" "$INSTALL_DIR/scripts/create_tinsnip_user.sh"
8080+ download_file "scripts/setup_rootless_docker.sh" "$INSTALL_DIR/scripts/setup_rootless_docker.sh"
8181+ download_file "scripts/deploy_service.sh" "$INSTALL_DIR/scripts/deploy_service.sh"
8282+8383+ # Download LLDAP service files
8484+ download_file "services/lldap/docker-compose.yml" "$INSTALL_DIR/services/lldap/docker-compose.yml"
8585+ download_file "services/lldap/setup.sh" "$INSTALL_DIR/services/lldap/setup.sh"
8686+ fi
8787+8888+ log "Installation complete!"
8989+ log ""
9090+ log "Next steps:"
9191+ log "1. cd $INSTALL_DIR"
9292+ log "2. ./setup.sh"
9393+}
9494+9595+main "$@"
+123
scripts/configure_boot_order.sh
···11+#!/bin/bash
22+33+# Configure boot ordering to ensure tinsnip services start first
44+55+set -euo pipefail
66+77+log() {
88+ echo "[Boot Order] $*"
99+}
1010+1111+create_tinsnip_target() {
1212+ log "Creating tinsnip systemd target..."
1313+1414+ # Create a systemd target that groups all tinsnip services
1515+ sudo tee /etc/systemd/system/tinsnip.target > /dev/null << 'EOF'
1616+[Unit]
1717+Description=tinsnip Infrastructure Services
1818+After=network-online.target
1919+Wants=network-online.target
2020+2121+[Install]
2222+WantedBy=multi-user.target
2323+EOF
2424+2525+ # Update tinsnip service files to be part of this target
2626+ for service in /etc/systemd/system/tinsnip-*.service; do
2727+ if [[ -f "$service" ]]; then
2828+ log "Updating $service to use tinsnip.target..."
2929+ sudo sed -i '/\[Install\]/,/WantedBy=/ s/WantedBy=.*/WantedBy=tinsnip.target/' "$service"
3030+ fi
3131+ done
3232+3333+ sudo systemctl daemon-reload
3434+ sudo systemctl enable tinsnip.target
3535+}
3636+3737+create_wait_for_tinsnip() {
3838+ log "Creating wait-for-tinsnip service..."
3939+4040+ # Create a service that waits for tinsnip services to be ready
4141+ sudo tee /etc/systemd/system/wait-for-tinsnip.service > /dev/null << 'EOF'
4242+[Unit]
4343+Description=Wait for tinsnip services to be ready
4444+After=tinsnip.target
4545+Wants=tinsnip.target
4646+4747+[Service]
4848+Type=oneshot
4949+RemainAfterExit=yes
5050+ExecStart=/bin/bash -c 'until nc -z localhost 3890; do echo "Waiting for LDAP..."; sleep 2; done; echo "LDAP is ready"'
5151+5252+[Install]
5353+WantedBy=multi-user.target
5454+EOF
5555+5656+ sudo systemctl daemon-reload
5757+ sudo systemctl enable wait-for-tinsnip.service
5858+}
5959+6060+configure_user_dependencies() {
6161+ log "Configuring user session dependencies..."
6262+6363+ # Create a drop-in for user@.service to depend on tinsnip
6464+ sudo mkdir -p /etc/systemd/system/user@.service.d
6565+6666+ # This affects ALL user sessions
6767+ sudo tee /etc/systemd/system/user@.service.d/wait-for-tinsnip.conf > /dev/null << 'EOF'
6868+[Unit]
6969+After=wait-for-tinsnip.service
7070+Wants=wait-for-tinsnip.service
7171+EOF
7272+7373+ # For more specific control, create a drop-in just for specific users
7474+ for uid in 1003 1004 1005; do # Add your regular user UIDs here
7575+ if id -u >/dev/null 2>&1; then
7676+ sudo mkdir -p "/etc/systemd/system/user@${uid}.service.d"
7777+ sudo tee "/etc/systemd/system/user@${uid}.service.d/wait-for-tinsnip.conf" > /dev/null << 'EOF'
7878+[Unit]
7979+After=wait-for-tinsnip.service
8080+Wants=wait-for-tinsnip.service
8181+EOF
8282+ fi
8383+ done
8484+8585+ sudo systemctl daemon-reload
8686+}
8787+8888+create_machine_dependencies() {
8989+ log "Creating machine service dependencies..."
9090+9191+ # If machine repo has systemd services, update them
9292+ # This is a template - adjust based on actual machine services
9393+ cat > /tmp/machine-services-dependency.conf << 'EOF'
9494+[Unit]
9595+After=wait-for-tinsnip.service
9696+Wants=wait-for-tinsnip.service
9797+EOF
9898+9999+ log "Template created at /tmp/machine-services-dependency.conf"
100100+ log "Apply this to any machine systemd services that need LDAP"
101101+}
102102+103103+main() {
104104+ log "Configuring boot order for tinsnip priority..."
105105+106106+ create_tinsnip_target
107107+ create_wait_for_tinsnip
108108+ configure_user_dependencies
109109+ create_machine_dependencies
110110+111111+ log ""
112112+ log "Boot order configuration complete!"
113113+ log ""
114114+ log "Boot sequence will be:"
115115+ log "1. System boot"
116116+ log "2. tinsnip.target (all tinsnip services)"
117117+ log "3. wait-for-tinsnip.service (confirms LDAP is ready)"
118118+ log "4. Regular user sessions (including machine services)"
119119+ log ""
120120+ log "To test: sudo systemctl list-dependencies tinsnip.target"
121121+}
122122+123123+main "$@"
+65
scripts/create_tinsnip_user.sh
···11+#!/bin/bash
22+33+# Create dedicated tinsnip user for running infrastructure services
44+55+set -euo pipefail
66+77+log() {
88+ echo "[User Setup] $*"
99+}
1010+1111+create_tinsnip_user() {
1212+ local username="tinsnip"
1313+ local uid=1010
1414+1515+ log "Creating dedicated user '$username' with UID $uid..."
1616+1717+ if id "$username" &>/dev/null; then
1818+ log "User $username already exists"
1919+ return 0
2020+ fi
2121+2222+ # Create regular user (not system user) for rootless Docker
2323+ sudo useradd -m -u "$uid" -s /bin/bash -c "tinsnip Infrastructure Services" "$username"
2424+ log "Created user $username with UID $uid"
2525+2626+ # Add subuid/subgid ranges for rootless Docker
2727+ echo "$username:100000:65536" | sudo tee -a /etc/subuid
2828+ echo "$username:100000:65536" | sudo tee -a /etc/subgid
2929+ log "Added subuid/subgid mappings for rootless containers"
3030+3131+ # Enable lingering for systemd user sessions
3232+ sudo loginctl enable-linger "$username"
3333+ log "Enabled systemd lingering for $username"
3434+3535+ # Create directory structure
3636+ sudo -u "$username" mkdir -p /home/"$username"/{services,config,data}
3737+ log "Created directory structure"
3838+}
3939+4040+create_system_users() {
4141+ log "Creating system users for services..."
4242+4343+ # Create lldap system user (will be used inside containers)
4444+ if ! id "lldap" &>/dev/null; then
4545+ sudo useradd -r -u 999 -s /bin/false -d /nonexistent -c "LLDAP Service" lldap
4646+ log "Created system user 'lldap' with UID 999"
4747+ else
4848+ log "System user 'lldap' already exists"
4949+ fi
5050+}
5151+5252+main() {
5353+ log "Setting up tinsnip user environment..."
5454+5555+ create_tinsnip_user
5656+ create_system_users
5757+5858+ log "User setup complete!"
5959+ log ""
6060+ log "Created users:"
6161+ log " - tinsnip (UID 1010): Runs rootless Docker and all services"
6262+ log " - lldap (UID 999): System user for LLDAP container"
6363+}
6464+6565+main "$@"
+105
scripts/deploy_service.sh
···11+#!/bin/bash
22+33+# Deploy a tinsnip service
44+55+set -euo pipefail
66+77+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
88+SERVICES_DIR="$(dirname "$SCRIPT_DIR")/services"
99+1010+log() {
1111+ echo "[Deploy] $*"
1212+}
1313+1414+error() {
1515+ log "ERROR: $*" >&2
1616+ exit 1
1717+}
1818+1919+deploy_service() {
2020+ local service_name="$1"
2121+ local service_dir="$SERVICES_DIR/$service_name"
2222+2323+ if [[ ! -d "$service_dir" ]]; then
2424+ error "Service directory not found: $service_dir"
2525+ fi
2626+2727+ log "Deploying service: $service_name"
2828+2929+ # Copy service files to tinsnip user's home
3030+ local target_dir="/home/tinsnip/services/$service_name"
3131+ sudo -u tinsnip mkdir -p "$target_dir"
3232+3333+ # Copy all files
3434+ sudo cp -r "$service_dir"/* "$target_dir/"
3535+ sudo chown -R tinsnip:tinsnip "$target_dir"
3636+3737+ # Make scripts executable
3838+ find "$target_dir" -name "*.sh" -exec sudo chmod +x {} \;
3939+4040+ # Run service-specific setup if it exists
4141+ if [[ -f "$target_dir/setup.sh" ]]; then
4242+ log "Running $service_name setup..."
4343+ sudo -u tinsnip -i bash -c "cd ~/services/$service_name && ./setup.sh"
4444+ else
4545+ # Generic docker-compose deployment
4646+ log "Starting $service_name with docker-compose..."
4747+ sudo -u tinsnip -i bash -c "cd ~/services/$service_name && docker compose up -d"
4848+ fi
4949+5050+ # Create systemd service for auto-start
5151+ create_systemd_service "$service_name"
5252+}
5353+5454+create_systemd_service() {
5555+ local service_name="$1"
5656+ local systemd_file="/etc/systemd/system/tinsnip-${service_name}.service"
5757+5858+ log "Creating systemd service for $service_name..."
5959+6060+ sudo tee "$systemd_file" > /dev/null << EOF
6161+[Unit]
6262+Description=tinsnip $service_name service
6363+After=network-online.target
6464+Wants=network-online.target
6565+Requires=user@1010.service
6666+6767+[Service]
6868+Type=simple
6969+User=tinsnip
7070+WorkingDirectory=/home/tinsnip/services/$service_name
7171+ExecStart=/usr/bin/sudo -u tinsnip docker compose up
7272+ExecStop=/usr/bin/sudo -u tinsnip docker compose down
7373+Restart=on-failure
7474+RestartSec=10
7575+7676+[Install]
7777+WantedBy=multi-user.target
7878+EOF
7979+8080+ sudo systemctl daemon-reload
8181+ sudo systemctl enable "tinsnip-${service_name}.service"
8282+ log "Systemd service created and enabled: tinsnip-${service_name}.service"
8383+}
8484+8585+main() {
8686+ if [[ $# -lt 1 ]]; then
8787+ error "Usage: $0 <service-name>"
8888+ fi
8989+9090+ local service_name="$1"
9191+9292+ # Check if tinsnip user exists
9393+ if ! id "tinsnip" &>/dev/null; then
9494+ error "tinsnip user does not exist. Run setup.sh first."
9595+ fi
9696+9797+ deploy_service "$service_name"
9898+9999+ log ""
100100+ log "Service $service_name deployed successfully!"
101101+ log "To check status: sudo -u tinsnip docker compose -f /home/tinsnip/services/$service_name/docker-compose.yml ps"
102102+ log "To view logs: sudo -u tinsnip docker compose -f /home/tinsnip/services/$service_name/docker-compose.yml logs"
103103+}
104104+105105+main "$@"
+112
scripts/setup_rootless_docker.sh
···11+#!/bin/bash
22+33+# Setup rootless Docker for tinsnip user
44+55+set -euo pipefail
66+77+log() {
88+ echo "[Docker Setup] $*"
99+}
1010+1111+install_docker_rootless() {
1212+ local username="tinsnip"
1313+1414+ log "Installing rootless Docker for $username..."
1515+1616+ # Check if Docker is already installed for the user
1717+ if sudo -u "$username" -i bash -c "command -v docker &>/dev/null"; then
1818+ log "Docker already installed for $username"
1919+ return 0
2020+ fi
2121+2222+ # Install dependencies
2323+ log "Installing Docker dependencies..."
2424+ sudo apt-get update -qq
2525+ sudo apt-get install -y \
2626+ uidmap \
2727+ dbus-user-session \
2828+ systemd-container \
2929+ fuse-overlayfs \
3030+ slirp4netns
3131+3232+ # Install Docker rootless as tinsnip user
3333+ log "Installing Docker rootless mode..."
3434+ sudo -u "$username" -i bash << 'EOF'
3535+# Download and run rootless installer
3636+curl -fsSL https://get.docker.com/rootless | sh
3737+3838+# Add Docker binaries to PATH
3939+echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
4040+echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
4141+4242+# Source the new PATH
4343+source ~/.bashrc
4444+4545+# Enable Docker service
4646+systemctl --user enable docker.service
4747+systemctl --user start docker.service
4848+4949+# Wait for Docker to start
5050+sleep 5
5151+5252+# Test Docker
5353+if docker version &>/dev/null; then
5454+ echo "Docker rootless installation successful"
5555+else
5656+ echo "Docker installation may have issues"
5757+ exit 1
5858+fi
5959+EOF
6060+6161+ if [[ $? -eq 0 ]]; then
6262+ log "Docker rootless installation completed successfully"
6363+ else
6464+ log "Warning: Docker installation may have issues"
6565+ fi
6666+}
6767+6868+configure_docker() {
6969+ local username="tinsnip"
7070+7171+ log "Configuring Docker for $username..."
7272+7373+ # Create Docker config directory
7474+ sudo -u "$username" mkdir -p /home/"$username"/.docker
7575+7676+ # Create daemon.json with useful defaults
7777+ sudo -u "$username" tee /home/"$username"/.docker/daemon.json > /dev/null << 'EOF'
7878+{
7979+ "log-driver": "json-file",
8080+ "log-opts": {
8181+ "max-size": "10m",
8282+ "max-file": "3"
8383+ },
8484+ "storage-driver": "overlay2"
8585+}
8686+EOF
8787+8888+ # Restart Docker to apply config
8989+ sudo -u "$username" systemctl --user restart docker.service
9090+9191+ log "Docker configuration complete"
9292+}
9393+9494+main() {
9595+ log "Setting up rootless Docker for tinsnip..."
9696+9797+ # Check if tinsnip user exists
9898+ if ! id "tinsnip" &>/dev/null; then
9999+ log "ERROR: tinsnip user does not exist. Run create_tinsnip_user.sh first."
100100+ exit 1
101101+ fi
102102+103103+ install_docker_rootless
104104+ configure_docker
105105+106106+ log "Rootless Docker setup complete!"
107107+ log ""
108108+ log "Docker is now running for user 'tinsnip'"
109109+ log "To verify: sudo -u tinsnip docker version"
110110+}
111111+112112+main "$@"