runtime

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 24, 2025 License: MIT Imports: 10 Imported by: 0

README

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

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DropPrivileges

func DropPrivileges(username string, group string) error

DropPrivileges permanently changes the process user and group to the specified unprivileged account for enhanced security.

This function is typically used by services that start as root but should run with minimal privileges. The Go runtime ensures all OS threads execute the underlying syscalls, making this safe for multi-threaded applications.

The function drops privileges in the correct order:

  1. Set group ID first (requires root privileges)
  2. Set user ID second (after this, root privileges are permanently lost)

Security notes:

  • This operation is permanent and irreversible within the process
  • Always verify the user/group exist before calling
  • Call this as early as possible after completing privileged operations
  • Both parameters are optional; empty strings skip that operation

Parameters:

  • username: Username to switch to (empty string skips user change)
  • group: Group name to switch to (empty string skips group change)

Returns:

  • error: nil on success, error if user/group lookup fails or syscall fails

Examples:

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

// Only change user, keep current group
if err := runtimeEnv.DropPrivileges("nobody", ""); err != nil {
    log.Fatalf("Failed to drop privileges: %v", err)
}

// Typical usage pattern
func main() {
    // Perform privileged operations (bind to port 80, etc.)
    if err := startServer(); err != nil {
        log.Fatal(err)
    }

    // Drop privileges before handling requests
    if err := runtimeEnv.DropPrivileges("www-data", "www-data"); err != nil {
        log.Fatal(err)
    }

    // Now running as unprivileged user
    serveRequests()
}

func LoadEnv

func LoadEnv(file string) error

LoadEnv reads a .env file and adds all variable definitions to the process environment.

The function supports a simple .env file format:

  • Comments: Lines starting with # are ignored
  • Empty lines: Blank lines are ignored
  • Export prefix: "export VAR=value" is supported (export is stripped)
  • Quoted values: Double-quoted strings support escape sequences
  • Key-value pairs: Must be in the format KEY=VALUE

Escape sequences in quoted strings:

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

Limitations:

  • Comments are only allowed at the start of lines (not inline)
  • Only double quotes are supported (not single quotes)
  • No variable expansion or substitution
  • No multi-line values

Parameters:

  • file: Path to the .env file to load

Returns:

  • error: nil on success, error if file cannot be read or contains invalid syntax

Examples:

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

// It's safe to ignore file-not-found errors
_ = runtimeEnv.LoadEnv("./.env") // Optional config file

Example .env file:

# Database configuration
DB_HOST=localhost
DB_PORT=5432
export DB_NAME=myapp
DB_PASSWORD="secret with spaces"
LOG_FORMAT="timestamp\tlevel\tmessage\n"

func SystemdNotify

func SystemdNotify(ready bool, status string)

SystemdNotify sends service status notifications to systemd when running as a systemd service (Type=notify).

This function implements the systemd notification protocol, allowing services to:

  • Signal readiness to accept requests (ready=true)
  • Update status messages visible in "systemctl status"
  • Implement proper service lifecycle management

The function safely handles non-systemd environments by checking for the NOTIFY_SOCKET environment variable. If not running under systemd, it returns immediately without error.

Errors from systemd-notify are intentionally ignored as there's limited recovery action and the service should continue running.

Parameters:

  • ready: If true, signals the service is ready (sends --ready)
  • status: Status message to display (empty string skips status update)

Examples:

// Signal service is ready
runtimeEnv.SystemdNotify(true, "Ready to accept connections")

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

// Shutdown notification
runtimeEnv.SystemdNotify(false, "Shutting down gracefully")

// Typical usage in main()
func main() {
    // Initialize application
    if err := initialize(); err != nil {
        log.Fatal(err)
    }

    // Signal systemd we're ready
    runtimeEnv.SystemdNotify(true, "Running")

    // Run service
    serve()
}

Systemd service file example:

[Service]
Type=notify
ExecStart=/usr/bin/myservice
NotifyAccess=main

See: https://www.freedesktop.org/software/systemd/man/sd_notify.html

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL