Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

runtimeEnv

The runtimeEnv package provides utilities for runtime environment setup and systemd integration for ClusterCockpit applications. It enables secure privilege management, environment configuration, and proper systemd service lifecycle integration.

Features

  • Environment file loading: Read and parse .env configuration files
  • Privilege dropping: Securely drop from root to unprivileged users
  • Systemd integration: Service readiness notifications and status updates
  • Thread-safe: All functions safe for concurrent use
  • Cross-platform: Works on Linux systems (privilege dropping is Linux-specific)

Installation

import "github.com/ClusterCockpit/cc-lib/v2/runtimeEnv"

Quick Start

package main

import (
    "log"
    "os"
    
    "github.com/ClusterCockpit/cc-lib/v2/runtimeEnv"
)

func main() {
    // Load optional .env file
    if err := runtimeEnv.LoadEnv("./.env"); err != nil && !os.IsNotExist(err) {
        log.Fatalf("Failed to load .env: %v", err)
    }
    
    // Start server (may require root for port < 1024)
    if err := startServer(":80"); err != nil {
        log.Fatal(err)
    }
    
    // Drop privileges for security
    if err := runtimeEnv.DropPrivileges("www-data", "www-data"); err != nil {
        log.Fatal(err)
    }
    
    // Notify systemd we're ready
    runtimeEnv.SystemdNotify(true, "Running")
    
    // Serve requests
    serve()
}

Functions

LoadEnv

Load environment variables from a .env file.

func LoadEnv(file string) error

Supported .env syntax:

# Comments (must be at start of line)
SIMPLE_VAR=value
export EXPORTED_VAR=value
QUOTED_VAR="value with spaces"
ESCAPED_VAR="line1\nline2\ttabbed"

Escape sequences in quoted strings:

  • \n - newline
  • \r - carriage return
  • \t - tab
  • \" - double quote

Limitations:

  • Comments only allowed at line start (not inline)
  • Only double quotes supported
  • No variable expansion/substitution
  • No multi-line values

Example:

// Load required .env file
if err := runtimeEnv.LoadEnv("config.env"); err != nil {
    log.Fatal(err)
}

// Load optional .env file
if err := runtimeEnv.LoadEnv(".env"); err != nil && !os.IsNotExist(err) {
    log.Fatalf("Failed to load .env: %v", err)
}

// Now use environment variables
dbHost := os.Getenv("DB_HOST")

Sample .env file:

# Database configuration
DB_HOST=localhost
DB_PORT=5432
export DB_NAME=clustercockpit
DB_PASSWORD="secret password with spaces"

# Logging
LOG_LEVEL=info
LOG_FORMAT="[%level%]\t%message%\n"

DropPrivileges

Permanently drop root privileges to an unprivileged user.

func DropPrivileges(username string, group string) error

Security best practices:

  1. Drop early: Call as soon as privileged operations complete
  2. Verify user exists: Ensure user/group exist before starting
  3. Irreversible: Cannot regain root privileges after calling
  4. Both or user only: Can drop both user+group or just user

Parameters:

  • username - Username to switch to (empty string skips)
  • group - Group name to switch to (empty string skips)

Example 1: Basic usage

// Drop to dedicated service user
if err := runtimeEnv.DropPrivileges("ccuser", "ccgroup"); err != nil {
    log.Fatalf("Failed to drop privileges: %v", err)
}

Example 2: Only change user

// Keep current group
if err := runtimeEnv.DropPrivileges("nobody", ""); err != nil {
    log.Fatal(err)
}

Example 3: Typical server pattern

func main() {
    // Bind to privileged port (requires root)
    listener, err := net.Listen("tcp", ":80")
    if err != nil {
        log.Fatal(err)
    }
    
    // Drop privileges before handling requests
    if err := runtimeEnv.DropPrivileges("www-data", "www-data"); err != nil {
        log.Fatal(err)
    }
    
    log.Println("Now running as www-data user")
    
    // Serve requests as unprivileged user
    http.Serve(listener, handler)
}

Example 4: Conditional privilege dropping

func main() {
    // Only drop if running as root
    if os.Geteuid() == 0 {
        log.Println("Running as root, dropping privileges")
        if err := runtimeEnv.DropPrivileges("ccuser", "ccgroup"); err != nil {
            log.Fatal(err)
        }
    } else {
        log.Println("Not running as root, keeping current user")
    }
}

SystemdNotify

Send status notifications to systemd.

func SystemdNotify(ready bool, status string)

Parameters:

  • ready - If true, signals service is ready (sends --ready)
  • status - Status message for systemctl status (optional)

Behavior:

  • Safe to call in non-systemd environments (checks NOTIFY_SOCKET)
  • Errors are ignored (service continues running)
  • Does nothing if not running under systemd

Example 1: Signal readiness

// After initialization completes
runtimeEnv.SystemdNotify(true, "Ready to accept connections")

Example 2: Status updates

// Update status without signaling ready
runtimeEnv.SystemdNotify(false, "Processing 1000 requests/sec")

Example 3: Shutdown notification

func main() {
    // Setup signal handling
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
    
    // Start service
    go serve()
    runtimeEnv.SystemdNotify(true, "Running")
    
    // Wait for shutdown signal
    <-sigChan
    runtimeEnv.SystemdNotify(false, "Shutting down gracefully")
    
    // Cleanup
    cleanup()
}

Example 4: Complete service lifecycle

func main() {
    log.Println("Initializing...")
    if err := initialize(); err != nil {
        log.Fatal(err)
    }
    
    log.Println("Starting server...")
    if err := startServer(); err != nil {
        log.Fatal(err)
    }
    
    // Signal systemd we're ready
    runtimeEnv.SystemdNotify(true, "Running")
    log.Println("Service ready")
    
    // Update status periodically
    ticker := time.NewTicker(30 * time.Second)
    go func() {
        for range ticker.C {
            stats := getStats()
            runtimeEnv.SystemdNotify(false, 
                fmt.Sprintf("Active connections: %d", stats.Connections))
        }
    }()
    
    // Run service
    serve()
}

Systemd Service Configuration

Basic service file:

[Unit]
Description=ClusterCockpit Service
After=network.target

[Service]
Type=notify
User=ccuser
Group=ccgroup
ExecStart=/usr/bin/myservice
NotifyAccess=main
Restart=on-failure

[Install]
WantedBy=multi-user.target

With environment file:

[Service]
Type=notify
EnvironmentFile=/etc/myservice/service.env
ExecStart=/usr/bin/myservice
NotifyAccess=main

Complete Examples

Example 1: ClusterCockpit Collector

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
    
    "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
    "github.com/ClusterCockpit/cc-lib/v2/runtimeEnv"
)

func main() {
    // Load optional config
    _ = runtimeEnv.LoadEnv("./.env")
    
    // Initialize logger
    ccLogger.Init(os.Getenv("LOG_LEVEL"), false)
    
    // Initialize collector
    ccLogger.Info("Initializing collector")
    if err := initCollector(); err != nil {
        ccLogger.Fatal(err)
    }
    
    // Drop privileges if running as root
    if os.Geteuid() == 0 {
        user := os.Getenv("RUN_USER")
        group := os.Getenv("RUN_GROUP")
        if user == "" {
            user = "nobody"
        }
        if group == "" {
            group = "nogroup"
        }
        
        if err := runtimeEnv.DropPrivileges(user, group); err != nil {
            ccLogger.Fatalf("Failed to drop privileges: %v", err)
        }
        ccLogger.Infof("Dropped privileges to %s:%s", user, group)
    }
    
    // Start collection
    ccLogger.Info("Starting metric collection")
    go collect()
    
    // Signal systemd
    runtimeEnv.SystemdNotify(true, "Collecting metrics")
    
    // Wait for shutdown signal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
    <-sigChan
    
    runtimeEnv.SystemdNotify(false, "Shutting down")
    ccLogger.Info("Shutdown complete")
}

Example 2: Web Server with Privilege Dropping

package main

import (
    "log"
    "net/http"
    "os"
    
    "github.com/ClusterCockpit/cc-lib/v2/runtimeEnv"
)

func main() {
    // Load config
    if err := runtimeEnv.LoadEnv("server.env"); err != nil {
        log.Fatal(err)
    }
    
    // Create listener on privileged port (requires root)
    port := os.Getenv("PORT")
    if port == "" {
        port = "80"
    }
    
    listener, err := net.Listen("tcp", ":"+port)
    if err != nil {
        log.Fatalf("Failed to bind to port %s: %v", port, err)
    }
    
    // Drop to unprivileged user
    if err := runtimeEnv.DropPrivileges("www-data", "www-data"); err != nil {
        log.Fatal(err)
    }
    log.Println("Privileges dropped to www-data")
    
    // Setup routes
    http.HandleFunc("/", handleRequest)
    
    // Notify systemd
    runtimeEnv.SystemdNotify(true, "Serving HTTP on :"+port)
    
    // Serve (already have listener from root)
    log.Fatal(http.Serve(listener, nil))
}

Error Handling

All functions return errors that should be checked:

// LoadEnv - handle file not found separately
if err := runtimeEnv.LoadEnv(".env"); err != nil {
    if os.IsNotExist(err) {
        log.Println("No .env file, using defaults")
    } else {
        log.Fatalf("Error loading .env: %v", err)
    }
}

// DropPrivileges - always fatal
if err := runtimeEnv.DropPrivileges("user", "group"); err != nil {
    log.Fatalf("Cannot drop privileges: %v", err)
}

// SystemdNotify - no return value, errors ignored internally
runtimeEnv.SystemdNotify(true, "Running")

Thread Safety

All functions are thread-safe and can be called from multiple goroutines. However:

  • LoadEnv: Safe to call concurrently, but typically called once at startup
  • DropPrivileges: Should only be called once during initialization
  • SystemdNotify: Safe to call frequently from multiple goroutines

Platform Notes

  • LoadEnv: Works on all platforms
  • DropPrivileges: Linux only (uses syscall.Setuid/Setgid)
  • SystemdNotify: Linux only (requires systemd), safe no-op on other platforms

Testing

The package includes comprehensive tests for all functions. Run tests with:

go test -v github.com/ClusterCockpit/cc-lib/v2/runtimeEnv

Security Considerations

  1. Privilege Dropping:

    • Always drop privileges as early as possible
    • Verify user/group exist before starting service
    • Test your service runs correctly as unprivileged user
    • Never try to regain privileges after dropping
  2. Environment Files:

    • Protect .env files with appropriate permissions (0600 or 0640)
    • Never commit .env files with secrets to version control
    • Use .env.example for templates without secrets
  3. Best Practices:

    • Use dedicated service users (not nobody/nogroup in production)
    • Run with minimal filesystem access
    • Use systemd's additional security features (PrivateTmp, NoNewPrivileges, etc.)

API Reference

For complete API documentation, see pkg.go.dev.

License

Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
Licensed under the MIT License. See LICENSE file for details.

See Also