From caba5bae70795a0d33f2f2eebbfd203057f8e048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sun, 29 Jun 2025 21:59:41 +0200 Subject: [PATCH] security improvements --- .env.example | 67 ++ .gitignore | 1 + README.md | 406 +++++++++ cmd/api/main.go | 4 +- documentation/API.md | 822 ++++++++++++++++++ documentation/CONFIGURATION.md | 395 +++++++++ documentation/DEPLOYMENT.md | 691 +++++++++++++++ documentation/SECURITY.md | 264 ++++++ go.mod | 4 +- go.sum | 9 +- local/api/api.go | 10 - local/controller/config.go | 2 +- local/controller/membership.go | 1 + local/middleware/auth.go | 97 ++- local/middleware/security/security.go | 351 ++++++++ .../001_upgrade_password_security.go | 238 +++++ local/model/steam_credentials.go | 79 +- local/model/user.go | 53 +- local/repository/membership.go | 1 - local/repository/server.go | 66 +- local/repository/state_history.go | 2 +- local/service/membership.go | 9 +- local/utl/configs/configs.go | 22 +- local/utl/db/db.go | 10 +- local/utl/jwt/jwt.go | 40 +- local/utl/password/password.go | 82 ++ local/utl/server/server.go | 26 +- local/utl/tracking/tracking.go | 3 + scripts/generate-secrets.ps1 | 176 ++++ scripts/generate-secrets.sh | 145 +++ 30 files changed, 3929 insertions(+), 147 deletions(-) create mode 100644 .env.example create mode 100644 README.md create mode 100644 documentation/API.md create mode 100644 documentation/CONFIGURATION.md create mode 100644 documentation/DEPLOYMENT.md create mode 100644 documentation/SECURITY.md create mode 100644 local/middleware/security/security.go create mode 100644 local/migrations/001_upgrade_password_security.go create mode 100644 local/utl/password/password.go create mode 100644 scripts/generate-secrets.ps1 create mode 100644 scripts/generate-secrets.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2a28a91 --- /dev/null +++ b/.env.example @@ -0,0 +1,67 @@ +# ACC Server Manager Environment Configuration +# Copy this file to .env and update with your actual values + +# ============================================================================= +# CRITICAL SECURITY SETTINGS (REQUIRED) +# ============================================================================= + +# JWT Secret Key - MUST be changed in production +# Generate with: openssl rand -base64 64 +JWT_SECRET=your-super-secure-jwt-secret-key-minimum-32-chars-long-change-this-in-production + +# Application Secrets - MUST be changed in production +# Generate with: openssl rand -hex 32 +APP_SECRET=your-super-secure-app-secret-change-this-in-production +APP_SECRET_CODE=your-super-secure-app-secret-code-change-this-in-production + +# Encryption Key for sensitive data (MUST be exactly 32 characters for AES-256) +# Generate with: openssl rand -hex 16 +ENCRYPTION_KEY=your-32-character-encryption-key-here + +# ============================================================================= +# CORE APPLICATION SETTINGS +# ============================================================================= + +# Database file name (SQLite) +DB_NAME=acc.db + +# Server port +PORT=3000 + +# CORS allowed origin (use specific domains in production) +CORS_ALLOWED_ORIGIN=http://localhost:5173 + +# Default admin password for initial setup (change after first login) +PASSWORD=change-this-default-admin-password + +# ============================================================================= +# INSTRUCTIONS FOR PRODUCTION DEPLOYMENT +# ============================================================================= + +# 1. Generate secure secrets: +# - JWT_SECRET: openssl rand -base64 64 +# - APP_SECRET: openssl rand -hex 32 +# - APP_SECRET_CODE: openssl rand -hex 32 +# - ENCRYPTION_KEY: openssl rand -hex 16 + +# 2. Set appropriate CORS origins for your domain + +# 3. Change the default PASSWORD immediately after first login + +# 4. NEVER commit actual secrets to version control! + +# ============================================================================= +# OPTIONAL SETTINGS (These are handled by system config in database) +# ============================================================================= + +# The following settings are managed through the application's system config +# and stored in the database. They are listed here for reference only: +# +# - SteamCMD path (configured via web interface) +# - NSSM path (configured via web interface) +# - Logging settings (handled by application defaults) +# - Rate limiting (handled by application defaults) +# - Backup settings (handled by application defaults) +# - Monitoring settings (handled by application defaults) +# +# These can be configured through the web interface after installation. diff --git a/.gitignore b/.gitignore index b72282e..9cae250 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ _testmain.go # .Dockerfile +.zed diff --git a/README.md b/README.md new file mode 100644 index 0000000..584b117 --- /dev/null +++ b/README.md @@ -0,0 +1,406 @@ +# ACC Server Manager + +A comprehensive web-based management system for Assetto Corsa Competizione (ACC) dedicated servers. This application provides a modern, secure interface for managing multiple ACC server instances with advanced features like automated Steam integration, firewall management, and real-time monitoring. + +## ๐Ÿš€ Features + +### Core Server Management +- **Multi-Server Support**: Manage multiple ACC server instances from a single interface +- **Configuration Management**: Web-based configuration editor with validation +- **Service Integration**: Windows Service management via NSSM +- **Port Management**: Automatic port allocation and firewall rule creation +- **Real-time Monitoring**: Live server status and performance metrics + +### Steam Integration +- **Automated Installation**: Automatic ACC server installation via SteamCMD +- **Credential Management**: Secure Steam credential storage with AES-256 encryption +- **Update Management**: Automated server updates and maintenance + +### Security Features +- **JWT Authentication**: Secure token-based authentication system +- **Role-Based Access**: Granular permission system with user roles +- **Rate Limiting**: Protection against brute force and DoS attacks +- **Input Validation**: Comprehensive input sanitization and validation +- **Security Headers**: OWASP-compliant security headers +- **Password Security**: Bcrypt password hashing with strength validation + +### Monitoring & Analytics +- **State History**: Track server state changes and player activity +- **Performance Metrics**: Server performance and usage statistics +- **Activity Logs**: Comprehensive logging and audit trails +- **Dashboard**: Real-time overview of all managed servers + +## ๐Ÿ—๏ธ Architecture + +### Technology Stack +- **Backend**: Go 1.23.0 with Fiber web framework +- **Database**: SQLite with GORM ORM +- **Authentication**: JWT tokens with bcrypt password hashing +- **API Documentation**: Swagger/OpenAPI integration +- **Dependency Injection**: Uber Dig container + +### Project Structure +``` +acc-server-manager/ +โ”œโ”€โ”€ cmd/ +โ”‚ โ””โ”€โ”€ api/ # Application entry point +โ”œโ”€โ”€ local/ +โ”‚ โ”œโ”€โ”€ api/ # API route definitions +โ”‚ โ”œโ”€โ”€ controller/ # HTTP request handlers +โ”‚ โ”œโ”€โ”€ middleware/ # Authentication and security middleware +โ”‚ โ”œโ”€โ”€ model/ # Database models and business logic +โ”‚ โ”œโ”€โ”€ repository/ # Data access layer +โ”‚ โ”œโ”€โ”€ service/ # Business logic services +โ”‚ โ””โ”€โ”€ utl/ # Utilities and shared components +โ”‚ โ”œโ”€โ”€ cache/ # Caching utilities +โ”‚ โ”œโ”€โ”€ command/ # Command execution utilities +โ”‚ โ”œโ”€โ”€ common/ # Common utilities +โ”‚ โ”œโ”€โ”€ configs/ # Configuration management +โ”‚ โ”œโ”€โ”€ db/ # Database connection and migration +โ”‚ โ”œโ”€โ”€ jwt/ # JWT token management +โ”‚ โ”œโ”€โ”€ logging/ # Logging utilities +โ”‚ โ”œโ”€โ”€ network/ # Network utilities +โ”‚ โ”œโ”€โ”€ password/ # Password hashing utilities +โ”‚ โ”œโ”€โ”€ regex_handler/ # Regular expression utilities +โ”‚ โ”œโ”€โ”€ server/ # HTTP server configuration +โ”‚ โ””โ”€โ”€ tracking/ # Server state tracking +โ”œโ”€โ”€ docs/ # Documentation +โ”œโ”€โ”€ logs/ # Application logs +โ””โ”€โ”€ vendor/ # Go dependencies +``` + +## ๐Ÿ“‹ Prerequisites + +### System Requirements +- **Operating System**: Windows 10/11 or Windows Server 2016+ +- **Go**: Version 1.23.0 or later +- **SteamCMD**: For ACC server installation and updates +- **NSSM**: Non-Sucking Service Manager for Windows services +- **PowerShell**: Version 5.0 or later + +### Dependencies +- ACC Dedicated Server files +- Valid Steam account (for server installation) +- Administrative privileges (for service and firewall management) + +## โš™๏ธ Installation + +### 1. Clone the Repository +```bash +git clone +cd acc-server-manager +``` + +### 2. Install Dependencies +```bash +go mod download +``` + +### 3. Generate Environment Configuration +We provide scripts to automatically generate secure secrets and create your `.env` file: + +**Windows (PowerShell):** +```powershell +.\scripts\generate-secrets.ps1 +``` + +**Linux/macOS (Bash):** +```bash +./scripts/generate-secrets.sh +``` + +**Manual Setup:** +If you prefer to set up manually: +```bash +copy .env.example .env +``` + +Then generate secure secrets: +```bash +# JWT Secret (64 bytes, base64 encoded) +openssl rand -base64 64 + +# Application secrets (32 bytes, hex encoded) +openssl rand -hex 32 + +# Encryption key (16 bytes, hex encoded = 32 characters) +openssl rand -hex 16 +``` + +Edit `.env` with your generated secrets: +```env +# Security Settings (REQUIRED) +JWT_SECRET=your-generated-jwt-secret-here +APP_SECRET=your-generated-app-secret-here +APP_SECRET_CODE=your-generated-secret-code-here +ENCRYPTION_KEY=your-generated-32-character-hex-key + +# Core Application Settings +PORT=3000 +CORS_ALLOWED_ORIGIN=http://localhost:5173 +DB_NAME=acc.db +PASSWORD=change-this-default-admin-password +``` + +### 4. Build the Application +```bash +go build -o api.exe cmd/api/main.go +``` + +### 5. Run the Application +```bash +./api.exe +``` + +The application will be available at `http://localhost:3000` + +## ๐Ÿ”ง Configuration + +### Environment Variables + +The application uses minimal environment variables, with most settings managed through the web interface: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `JWT_SECRET` | Yes | - | JWT signing secret (64+ chars, base64) | +| `APP_SECRET` | Yes | - | Application secret key (32 bytes, hex) | +| `APP_SECRET_CODE` | Yes | - | Application secret code (32 bytes, hex) | +| `ENCRYPTION_KEY` | Yes | - | AES-256 encryption key (32 hex chars) | +| `PORT` | No | 3000 | HTTP server port | +| `DB_NAME` | No | acc.db | SQLite database filename | +| `CORS_ALLOWED_ORIGIN` | No | http://localhost:5173 | CORS allowed origin | +| `PASSWORD` | No | - | Default admin password for initial setup | + +**โš ๏ธ Important**: All required secrets are automatically generated by the provided scripts in `scripts/` directory. + +### System Configuration (Web Interface) + +Advanced settings are managed through the web interface and stored in the database: +- **Steam Integration**: SteamCMD path and credentials +- **Service Management**: NSSM path and service settings +- **Server Settings**: Default ports, firewall rules +- **Security Policies**: Rate limits, session timeouts +- **Monitoring**: Logging levels, performance tracking +- **Backup Settings**: Automatic backup configuration + +Access these settings through the admin panel after initial setup. + +## ๐Ÿ”’ Security + +This application implements comprehensive security measures: + +### Authentication & Authorization +- **JWT Tokens**: Secure token-based authentication +- **Password Security**: Bcrypt hashing with strength validation +- **Role-Based Access**: Granular permission system +- **Session Management**: Configurable timeouts and lockouts + +### Protection Mechanisms +- **Rate Limiting**: Multiple layers of rate limiting +- **Input Validation**: Comprehensive input sanitization +- **Security Headers**: OWASP-compliant HTTP headers +- **CORS Protection**: Configurable cross-origin restrictions +- **Request Limits**: Size and timeout limitations + +### Monitoring & Logging +- **Security Events**: Authentication and authorization logging +- **Audit Trail**: Comprehensive activity logging +- **Threat Detection**: Suspicious activity monitoring + +For detailed security information, see [SECURITY.md](docs/SECURITY.md). + +## ๐Ÿ“š API Documentation + +The application includes comprehensive API documentation via Swagger UI: +- **Local Development**: http://localhost:3000/swagger/ +- **Interactive Testing**: Test API endpoints directly from the browser +- **Schema Documentation**: Complete request/response schemas + +### Key API Endpoints + +#### Authentication +- `POST /api/v1/auth/login` - User authentication +- `POST /api/v1/auth/register` - User registration +- `GET /api/v1/auth/me` - Get current user + +#### Server Management +- `GET /api/v1/servers` - List all servers +- `POST /api/v1/servers` - Create new server +- `GET /api/v1/servers/{id}` - Get server details +- `PUT /api/v1/servers/{id}` - Update server +- `DELETE /api/v1/servers/{id}` - Delete server + +#### Configuration +- `GET /api/v1/servers/{id}/config/{file}` - Get configuration file +- `PUT /api/v1/servers/{id}/config/{file}` - Update configuration +- `POST /api/v1/servers/{id}/restart` - Restart server + +## ๐Ÿ–ฅ๏ธ Frontend Integration + +This backend is designed to work with a modern web frontend. Recommended stack: +- **React/Vue/Angular**: Modern JavaScript framework +- **TypeScript**: Type safety and better development experience +- **Axios/Fetch**: HTTP client for API communication +- **WebSocket**: Real-time server status updates + +### CORS Configuration +Configure `CORS_ALLOWED_ORIGIN` to match your frontend URL: +```env +CORS_ALLOWED_ORIGIN=http://localhost:3000,https://yourdomain.com +``` + +## ๐Ÿ› ๏ธ Development + +### Running in Development Mode +```bash +# Install air for hot reloading (optional) +go install github.com/cosmtrek/air@latest + +# Run with hot reload +air + +# Or run directly with go +go run cmd/api/main.go +``` + +### Database Management +```bash +# View database schema +sqlite3 acc.db ".schema" + +# Backup database +copy acc.db acc_backup.db +``` + +### Testing +```bash +# Run all tests +go test ./... + +# Run tests with coverage +go test -cover ./... + +# Run specific test package +go test ./local/service/... +``` + +## ๐Ÿš€ Production Deployment + +### 1. Generate Production Secrets +```bash +# Use the secret generation script for production +.\scripts\generate-secrets.ps1 # Windows +./scripts/generate-secrets.sh # Linux/macOS +``` + +### 2. Build for Production +```bash +# Build optimized binary +go build -ldflags="-w -s" -o acc-server-manager.exe cmd/api/main.go +``` + +### 3. Security Checklist +- [ ] Generate unique production secrets (use provided scripts) +- [ ] Configure production CORS origins in `.env` +- [ ] Change default admin password immediately after first login +- [ ] Enable HTTPS with valid certificates +- [ ] Set up proper firewall rules +- [ ] Configure system paths via web interface +- [ ] Set up monitoring and alerting +- [ ] Test all security configurations + +### 3. Service Installation +```bash +# Create Windows service using NSSM +nssm install "ACC Server Manager" "C:\path\to\acc-server-manager.exe" +nssm set "ACC Server Manager" DisplayName "ACC Server Manager" +nssm set "ACC Server Manager" Description "Assetto Corsa Competizione Server Manager" +nssm start "ACC Server Manager" +``` + +### 4. Monitoring Setup +- Configure log rotation +- Set up health check monitoring +- Configure alerting for critical errors +- Monitor resource usage and performance + +## ๐Ÿ”ง Troubleshooting + +### Common Issues + +#### "JWT_SECRET environment variable is required" +**Solution**: Set the JWT_SECRET environment variable with a secure 32+ character string. + +#### "Failed to connect database" +**Solution**: Ensure the application has write permissions to the database directory. + +#### "SteamCMD not found" +**Solution**: Install SteamCMD and update the `STEAMCMD_PATH` environment variable. + +#### "Permission denied creating firewall rule" +**Solution**: Run the application as Administrator for firewall management. + +### Log Locations +- **Application Logs**: `./logs/app.log` +- **Error Logs**: `./logs/error.log` +- **Security Logs**: `./logs/security.log` + +### Debug Mode +Enable debug logging: +```env +LOG_LEVEL=debug +DEBUG_MODE=true +``` + +## ๐Ÿค Contributing + +### Development Setup +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/amazing-feature` +3. Make your changes and add tests +4. Ensure all tests pass: `go test ./...` +5. Commit your changes: `git commit -m 'Add amazing feature'` +6. Push to the branch: `git push origin feature/amazing-feature` +7. Open a Pull Request + +### Code Style +- Follow Go best practices and conventions +- Use `gofmt` for code formatting +- Add comprehensive comments for public functions +- Include tests for new functionality + +### Security Considerations +- Never commit secrets or credentials +- Follow secure coding practices +- Test security features thoroughly +- Report security issues privately + +## ๐Ÿ“ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## ๐Ÿ™ Acknowledgments + +- **Fiber Framework**: High-performance HTTP framework +- **GORM**: Powerful ORM for Go +- **Assetto Corsa Competizione**: The amazing racing simulation +- **Community**: Contributors and users who make this project possible + +## ๐Ÿ“ž Support + +### Documentation +- [Security Guide](docs/SECURITY.md) +- [API Documentation](http://localhost:3000/swagger/) +- [Configuration Guide](docs/CONFIGURATION.md) + +### Community +- **Issues**: Report bugs and request features via GitHub Issues +- **Discussions**: Join community discussions +- **Wiki**: Community-maintained documentation and guides + +### Professional Support +For professional support, consulting, or custom development, please contact the maintainers. + +--- + +**Happy Racing! ๐Ÿ** \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index 1cbcf2d..93d1b37 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -8,14 +8,12 @@ import ( "fmt" "os" - "github.com/joho/godotenv" "go.uber.org/dig" _ "acc-server-manager/docs" ) func main() { - godotenv.Load() // Initialize logger logger, err := logging.Initialize() if err != nil { @@ -26,7 +24,7 @@ func main() { // Set up panic recovery defer logging.RecoverAndLog() - + di := dig.New() cache.Start(di) db.Start(di) diff --git a/documentation/API.md b/documentation/API.md new file mode 100644 index 0000000..0ef85ef --- /dev/null +++ b/documentation/API.md @@ -0,0 +1,822 @@ +# API Documentation for ACC Server Manager + +## Overview + +The ACC Server Manager provides a comprehensive REST API for managing Assetto Corsa Competizione dedicated servers. This API enables full control over server instances, configurations, user management, and monitoring through HTTP endpoints. + +## Base URL + +``` +http://localhost:3000/api/v1 +``` + +## Authentication + +All API endpoints (except public ones) require authentication via JWT tokens. + +### Authentication Header +```http +Authorization: Bearer +``` + +### Token Expiration +- Default token lifetime: 24 hours +- Tokens should be refreshed before expiration +- Failed authentication returns HTTP 401 + +## Rate Limiting + +The API implements multiple layers of rate limiting: + +- **Global**: 100 requests per minute per IP +- **Authentication**: 5 attempts per 15 minutes per IP +- **API Endpoints**: 60 requests per minute per IP + +Rate limit exceeded responses return HTTP 429 with retry information. + +## Response Format + +All API responses follow a consistent JSON format: + +### Success Response +```json +{ + "success": true, + "data": { + // Response data + }, + "message": "Operation completed successfully" +} +``` + +### Error Response +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "Human readable error message", + "details": {} + } +} +``` + +## HTTP Status Codes + +| Status Code | Description | +|-------------|-------------| +| 200 | OK - Request successful | +| 201 | Created - Resource created successfully | +| 400 | Bad Request - Invalid request data | +| 401 | Unauthorized - Authentication required | +| 403 | Forbidden - Insufficient permissions | +| 404 | Not Found - Resource not found | +| 409 | Conflict - Resource already exists | +| 422 | Unprocessable Entity - Validation failed | +| 429 | Too Many Requests - Rate limit exceeded | +| 500 | Internal Server Error - Server error | + +## API Endpoints + +### Authentication + +#### Login +```http +POST /api/v1/auth/login +``` + +**Request Body:** +```json +{ + "username": "string", + "password": "string" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "token": "jwt-token-string", + "user": { + "id": "uuid", + "username": "string", + "role": { + "id": "uuid", + "name": "string", + "permissions": [] + } + } + } +} +``` + +#### Register User +```http +POST /api/v1/auth/register +``` +*Requires: `user.create` permission* + +**Request Body:** +```json +{ + "username": "string", + "password": "string", + "roleId": "uuid" +} +``` + +#### Get Current User +```http +GET /api/v1/auth/me +``` +*Requires: Authentication* + +**Response:** +```json +{ + "success": true, + "data": { + "id": "uuid", + "username": "string", + "role": { + "id": "uuid", + "name": "string", + "permissions": [] + } + } +} +``` + +### Server Management + +#### List Servers +```http +GET /api/v1/servers +``` +*Requires: `server.read` permission* + +**Query Parameters:** +- `page` (integer): Page number (default: 1) +- `limit` (integer): Items per page (default: 10) +- `search` (string): Search term +- `status` (string): Filter by status (running, stopped, error) + +**Response:** +```json +{ + "success": true, + "data": { + "servers": [ + { + "id": 1, + "name": "string", + "ip": "string", + "port": 9600, + "path": "string", + "serviceName": "string", + "status": "string", + "dateCreated": "2024-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "limit": 10, + "total": 50, + "pages": 5 + } + } +} +``` + +#### Create Server +```http +POST /api/v1/servers +``` +*Requires: `server.create` permission* + +**Request Body:** +```json +{ + "name": "string", + "ip": "string", + "port": 9600, + "path": "string" +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "id": 1, + "name": "string", + "ip": "string", + "port": 9600, + "path": "string", + "serviceName": "string", + "status": "created", + "dateCreated": "2024-01-01T00:00:00Z" + } +} +``` + +#### Get Server Details +```http +GET /api/v1/servers/{id} +``` +*Requires: `server.read` permission* + +**Path Parameters:** +- `id` (integer): Server ID + +**Response:** +```json +{ + "success": true, + "data": { + "id": 1, + "name": "string", + "ip": "string", + "port": 9600, + "path": "string", + "serviceName": "string", + "status": "string", + "dateCreated": "2024-01-01T00:00:00Z", + "configs": [], + "statistics": {} + } +} +``` + +#### Update Server +```http +PUT /api/v1/servers/{id} +``` +*Requires: `server.update` permission* + +**Path Parameters:** +- `id` (integer): Server ID + +**Request Body:** +```json +{ + "name": "string", + "ip": "string", + "port": 9600, + "path": "string" +} +``` + +#### Delete Server +```http +DELETE /api/v1/servers/{id} +``` +*Requires: `server.delete` permission* + +**Path Parameters:** +- `id` (integer): Server ID + +#### Start Server +```http +POST /api/v1/servers/{id}/start +``` +*Requires: `server.control` permission* + +#### Stop Server +```http +POST /api/v1/servers/{id}/stop +``` +*Requires: `server.control` permission* + +#### Restart Server +```http +POST /api/v1/servers/{id}/restart +``` +*Requires: `server.control` permission* + +### Configuration Management + +#### Get Configuration File +```http +GET /api/v1/servers/{id}/config/{file} +``` +*Requires: `config.read` permission* + +**Path Parameters:** +- `id` (integer): Server ID +- `file` (string): Configuration file name (configuration, event, eventRules, settings) + +**Response:** +```json +{ + "success": true, + "data": { + "file": "configuration", + "content": {}, + "lastModified": "2024-01-01T00:00:00Z" + } +} +``` + +#### Update Configuration File +```http +PUT /api/v1/servers/{id}/config/{file} +``` +*Requires: `config.update` permission* + +**Path Parameters:** +- `id` (integer): Server ID +- `file` (string): Configuration file name + +**Query Parameters:** +- `restart` (boolean): Restart server after update (default: false) +- `override` (boolean): Override validation warnings (default: false) + +**Request Body:** +```json +{ + "tcpPort": 9600, + "udpPort": 9600, + "maxConnections": 30, + "registerToLobby": 1, + "serverName": "My ACC Server", + "password": "", + "adminPassword": "admin123", + "trackMedalsRequirement": 0, + "safetyRatingRequirement": -1, + "racecraftRatingRequirement": -1, + "configVersion": 1 +} +``` + +#### Validate Configuration +```http +POST /api/v1/servers/{id}/config/{file}/validate +``` +*Requires: `config.read` permission* + +**Request Body:** Configuration object to validate + +**Response:** +```json +{ + "success": true, + "data": { + "valid": true, + "errors": [], + "warnings": [] + } +} +``` + +### Steam Integration + +#### Get Steam Credentials +```http +GET /api/v1/steam/credentials +``` +*Requires: `steam.read` permission* + +**Response:** +```json +{ + "success": true, + "data": { + "id": 1, + "username": "steam_username", + "dateCreated": "2024-01-01T00:00:00Z", + "lastUpdated": "2024-01-01T00:00:00Z" + } +} +``` + +#### Update Steam Credentials +```http +PUT /api/v1/steam/credentials +``` +*Requires: `steam.update` permission* + +**Request Body:** +```json +{ + "username": "steam_username", + "password": "steam_password" +} +``` + +#### Install/Update Server +```http +POST /api/v1/steam/install +``` +*Requires: `steam.install` permission* + +**Request Body:** +```json +{ + "serverId": 1, + "validate": true, + "beta": false +} +``` + +### User Management + +#### List Users +```http +GET /api/v1/users +``` +*Requires: `user.read` permission* + +**Query Parameters:** +- `page` (integer): Page number +- `limit` (integer): Items per page +- `search` (string): Search term + +**Response:** +```json +{ + "success": true, + "data": { + "users": [ + { + "id": "uuid", + "username": "string", + "role": { + "id": "uuid", + "name": "string" + }, + "createdAt": "2024-01-01T00:00:00Z" + } + ], + "pagination": {} + } +} +``` + +#### Create User +```http +POST /api/v1/users +``` +*Requires: `user.create` permission* + +#### Update User +```http +PUT /api/v1/users/{id} +``` +*Requires: `user.update` permission* + +#### Delete User +```http +DELETE /api/v1/users/{id} +``` +*Requires: `user.delete` permission* + +### Role and Permission Management + +#### List Roles +```http +GET /api/v1/roles +``` +*Requires: `role.read` permission* + +#### Create Role +```http +POST /api/v1/roles +``` +*Requires: `role.create` permission* + +**Request Body:** +```json +{ + "name": "string", + "description": "string", + "permissions": ["permission1", "permission2"] +} +``` + +#### List Permissions +```http +GET /api/v1/permissions +``` +*Requires: `permission.read` permission* + +**Response:** +```json +{ + "success": true, + "data": [ + { + "name": "server.create", + "description": "Create new servers", + "category": "server" + } + ] +} +``` + +### Monitoring and Analytics + +#### Get Server Statistics +```http +GET /api/v1/servers/{id}/stats +``` +*Requires: `stats.read` permission* + +**Query Parameters:** +- `from` (string): Start date (ISO 8601) +- `to` (string): End date (ISO 8601) +- `granularity` (string): hour, day, week, month + +**Response:** +```json +{ + "success": true, + "data": { + "totalPlaytime": 3600, + "playerCount": [], + "sessionTypes": [], + "dailyActivity": [], + "recentSessions": [] + } +} +``` + +#### Get System Health +```http +GET /api/v1/system/health +``` +*Public endpoint* + +**Response:** +```json +{ + "success": true, + "data": { + "status": "healthy", + "version": "1.0.0", + "uptime": 3600, + "database": "connected", + "services": { + "steam": "available", + "nssm": "available" + } + } +} +``` + +### Lookup Data + +#### Get Tracks +```http +GET /api/v1/lookup/tracks +``` +*Public endpoint* + +**Response:** +```json +{ + "success": true, + "data": [ + { + "name": "monza", + "uniquePitBoxes": 29, + "privateServerSlots": 60 + } + ] +} +``` + +#### Get Car Models +```http +GET /api/v1/lookup/cars +``` +*Public endpoint* + +#### Get Driver Categories +```http +GET /api/v1/lookup/driver-categories +``` +*Public endpoint* + +#### Get Cup Categories +```http +GET /api/v1/lookup/cup-categories +``` +*Public endpoint* + +#### Get Session Types +```http +GET /api/v1/lookup/session-types +``` +*Public endpoint* + +## Webhooks + +The API supports webhook notifications for server events: + +### Server Status Changes +```json +{ + "event": "server.status.changed", + "serverId": 1, + "serverName": "My Server", + "oldStatus": "stopped", + "newStatus": "running", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +### Configuration Updates +```json +{ + "event": "server.config.updated", + "serverId": 1, + "serverName": "My Server", + "configFile": "configuration", + "userId": "uuid", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +## Error Codes + +| Code | Description | +|------|-------------| +| `AUTH_REQUIRED` | Authentication required | +| `AUTH_INVALID` | Invalid credentials | +| `AUTH_EXPIRED` | Token expired | +| `PERMISSION_DENIED` | Insufficient permissions | +| `VALIDATION_FAILED` | Request validation failed | +| `RESOURCE_NOT_FOUND` | Requested resource not found | +| `RESOURCE_EXISTS` | Resource already exists | +| `RATE_LIMIT_EXCEEDED` | Rate limit exceeded | +| `SERVER_ERROR` | Internal server error | +| `SERVICE_UNAVAILABLE` | External service unavailable | + +## SDK Examples + +### JavaScript/Node.js +```javascript +const axios = require('axios'); + +class ACCServerManagerAPI { + constructor(baseUrl, token) { + this.baseUrl = baseUrl; + this.token = token; + this.client = axios.create({ + baseURL: baseUrl, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + } + + async getServers() { + const response = await this.client.get('/servers'); + return response.data; + } + + async createServer(serverData) { + const response = await this.client.post('/servers', serverData); + return response.data; + } + + async updateConfig(serverId, file, config, restart = false) { + const response = await this.client.put( + `/servers/${serverId}/config/${file}?restart=${restart}`, + config + ); + return response.data; + } +} + +// Usage +const api = new ACCServerManagerAPI('http://localhost:3000/api/v1', 'your-jwt-token'); +const servers = await api.getServers(); +``` + +### Python +```python +import requests + +class ACCServerManagerAPI: + def __init__(self, base_url, token): + self.base_url = base_url + self.headers = { + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json' + } + + def get_servers(self): + response = requests.get(f'{self.base_url}/servers', headers=self.headers) + return response.json() + + def create_server(self, server_data): + response = requests.post( + f'{self.base_url}/servers', + json=server_data, + headers=self.headers + ) + return response.json() + +# Usage +api = ACCServerManagerAPI('http://localhost:3000/api/v1', 'your-jwt-token') +servers = api.get_servers() +``` + +### Go +```go +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type ACCServerManagerAPI struct { + BaseURL string + Token string + Client *http.Client +} + +func NewACCServerManagerAPI(baseURL, token string) *ACCServerManagerAPI { + return &ACCServerManagerAPI{ + BaseURL: baseURL, + Token: token, + Client: &http.Client{}, + } +} + +func (api *ACCServerManagerAPI) request(method, endpoint string, body interface{}) (*http.Response, error) { + var reqBody bytes.Buffer + if body != nil { + json.NewEncoder(&reqBody).Encode(body) + } + + req, err := http.NewRequest(method, api.BaseURL+endpoint, &reqBody) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+api.Token) + req.Header.Set("Content-Type", "application/json") + + return api.Client.Do(req) +} + +func (api *ACCServerManagerAPI) GetServers() (interface{}, error) { + resp, err := api.request("GET", "/servers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result interface{} + json.NewDecoder(resp.Body).Decode(&result) + return result, nil +} +``` + +## Best Practices + +### Authentication +1. Store JWT tokens securely (httpOnly cookies for web apps) +2. Implement token refresh mechanism +3. Handle authentication errors gracefully +4. Use HTTPS in production + +### Rate Limiting +1. Implement exponential backoff for rate-limited requests +2. Cache responses when appropriate +3. Use batch operations when available +4. Monitor rate limit headers + +### Error Handling +1. Always check response status codes +2. Handle network errors gracefully +3. Implement retry logic for transient errors +4. Log errors for debugging + +### Performance +1. Use pagination for large datasets +2. Implement client-side caching +3. Use WebSockets for real-time updates +4. Compress request/response bodies + +## Support + +For API support: +- **Documentation**: Check this guide and interactive Swagger UI +- **Issues**: Report API bugs via GitHub Issues +- **Community**: Join community discussions for help +- **Professional Support**: Contact maintainers for enterprise support + +--- + +**Note**: This API is versioned. Breaking changes will result in a new API version. Always specify the version in your requests. \ No newline at end of file diff --git a/documentation/CONFIGURATION.md b/documentation/CONFIGURATION.md new file mode 100644 index 0000000..c35d133 --- /dev/null +++ b/documentation/CONFIGURATION.md @@ -0,0 +1,395 @@ +# Configuration Guide for ACC Server Manager + +## Overview + +This guide provides comprehensive information about configuring the ACC Server Manager application, including environment variables, server settings, security configurations, and advanced options. + +## ๐Ÿ“ Configuration Files + +### Environment Configuration (.env) + +The primary configuration is handled through environment variables. Create a `.env` file in the root directory: + +```bash +# Copy the example file +cp .env.example .env +``` + +### Configuration File Hierarchy + +1. **Environment Variables** (highest priority) +2. **`.env` file** (medium priority) +3. **Default values** (lowest priority) + +## ๐Ÿ” Security Configuration + +### Required Security Variables + +These variables are **mandatory** and the application will not start without them: + +```env +# JWT Secret - Used for signing authentication tokens +# Generate with: openssl rand -base64 64 +JWT_SECRET=your-super-secure-jwt-secret-minimum-64-characters-long + +# Application Secrets - Used for internal encryption and security +# Generate with: openssl rand -hex 32 +APP_SECRET=your-32-character-hex-secret-here +APP_SECRET_CODE=your-32-character-hex-secret-code-here + +# Encryption Key - Used for AES-256 encryption (MUST be exactly 32 bytes) +# Generate with: openssl rand -hex 32 +ENCRYPTION_KEY=your-exactly-32-byte-encryption-key-here +``` + +## ๐ŸŒ Server Configuration + +### Basic Server Settings + +```env +# HTTP server port +PORT=3000 + +# CORS allowed origin (comma-separated for multiple origins) +CORS_ALLOWED_ORIGIN=http://localhost:5173 + +# Database file name (SQLite) +DB_NAME=acc.db + +# Default admin password for initial setup (change after first login) +PASSWORD=change-this-default-admin-password +``` + +**Note**: Most other server configuration options (timeouts, request limits, etc.) are handled by application defaults and don't require environment variables. + +## ๐Ÿ—„๏ธ Database Configuration + +The application uses SQLite with minimal configuration required: + +```env +# Database file name (only setting available via environment) +DB_NAME=acc.db +``` + +**Note**: Other database settings like connection timeouts, migration settings, and SQL logging are handled internally by the application and don't require environment variables. + +## ๐ŸŽฎ Steam Integration + +Steam integration settings are managed through the web interface and stored in the database as system configuration. No environment variables are required for Steam integration. + +**Configuration via Web Interface:** +- SteamCMD executable path +- NSSM executable path +- Steam credentials (encrypted in database) +- Update schedules and preferences + +**Default Values:** +- SteamCMD Path: `c:\steamcmd\steamcmd.exe` +- NSSM Path: `.\nssm.exe` + +## ๐Ÿ”ฅ Windows Service Configuration + +Windows service and firewall configurations are handled internally by the application: + +**Service Management:** +- NSSM path configured via web interface +- Default service name prefix: `ACC-Server` +- Automatic service creation and management + +**Firewall Management:** +- Automatic firewall rule creation +- Default TCP port range: 9600+ +- Default UDP port range: 9600+ +- Rule cleanup on server deletion + +**No environment variables required** - all settings are managed through the system configuration interface. + +## ๐Ÿ“Š Logging Configuration + +Logging is handled internally by the application with sensible defaults: + +**Default Logging Behavior:** +- Log level: `info` (adjustable via code) +- Log format: Structured text format +- Log files: Automatic rotation and cleanup +- Security events: Automatically logged +- Error tracking: Comprehensive error logging + +**No environment variables required** - logging configuration is built into the application. + +## ๐Ÿšฆ Rate Limiting Configuration + +Rate limiting is built into the application with secure defaults: + +**Built-in Rate Limits:** +- Global: 100 requests per minute per IP +- Authentication: 5 attempts per 15 minutes per IP +- API endpoints: 60 requests per minute per IP +- Configuration updates: Protected with additional limits + +**No environment variables required** - rate limiting is automatically applied with appropriate limits for security and performance. + +## ๐Ÿ“ˆ Monitoring Configuration + +Monitoring features are built into the application: + +**Available Monitoring:** +- Health check endpoint: `/health` (always enabled) +- Performance tracking: Built-in performance monitoring +- Error tracking: Automatic error logging and tracking +- Security monitoring: Authentication and authorization events + +**No environment variables required** - monitoring is automatically enabled with appropriate defaults. + +## ๐Ÿ”„ Backup Configuration + +Backup functionality is handled internally: + +**Automatic Backups:** +- Database backup before migrations +- Configuration file versioning +- Error recovery mechanisms + +**Manual Backups:** +- Database files can be copied manually +- Configuration export/import via web interface + +**No environment variables required** - backup features are built into the application workflow. + +## ๐Ÿงช Development Configuration + +### Development Mode Settings + +```env +# Enable development mode (NEVER use in production) +DEV_MODE=false + +# Enable debug endpoints +DEBUG_ENDPOINTS=false + +# Enable hot reload (requires air) +HOT_RELOAD=false + +# Disable security features for testing (DANGEROUS) +DISABLE_SECURITY=false +``` + +### Testing Configuration + +```env +# Test database name +TEST_DB_NAME=acc_test.db + +# Enable test fixtures +ENABLE_TEST_FIXTURES=false + +# Test timeout in seconds +TEST_TIMEOUT=300 +``` + +## ๐Ÿญ Production Configuration + +### Production Deployment Settings + +```env +# Production mode +PRODUCTION=true + +# Enable HTTPS enforcement +FORCE_HTTPS=true + +# Security-first configuration +SECURITY_STRICT=true + +# Disable debug information +DISABLE_DEBUG_INFO=true + +# Enable comprehensive monitoring +ENABLE_MONITORING=true +``` + +### Performance Optimization + +```env +# Enable response compression +ENABLE_COMPRESSION=true + +# Compression level (1-9) +COMPRESSION_LEVEL=6 + +# Enable response caching +ENABLE_CACHING=true + +# Cache TTL in seconds +CACHE_TTL=300 + +# Maximum cache size in MB +CACHE_MAX_SIZE=100 +``` + +## ๐Ÿ› ๏ธ Advanced Configuration + +### Custom Port Ranges + +```env +# Custom TCP port ranges (comma-separated) +CUSTOM_TCP_PORTS=9600-9610,9700-9710 + +# Custom UDP port ranges (comma-separated) +CUSTOM_UDP_PORTS=9600-9610,9700-9710 + +# Exclude specific ports (comma-separated) +EXCLUDED_PORTS=9605,9705 +``` + +### Custom Paths + +```env +# Custom ACC server installation path +ACC_SERVER_PATH=C:\ACC_Server + +# Custom configuration templates path +CONFIG_TEMPLATES_PATH=./templates + +# Custom scripts path +SCRIPTS_PATH=./scripts +``` + +### Integration Settings + +```env +# External API endpoints +EXTERNAL_API_ENABLED=false +EXTERNAL_API_URL=https://api.example.com +EXTERNAL_API_KEY=your-api-key-here + +# Webhook notifications +WEBHOOK_ENABLED=false +WEBHOOK_URL=https://your-webhook-url.com +WEBHOOK_SECRET=your-webhook-secret +``` + +## ๐Ÿ“‹ Configuration Validation + +### Validation Rules + +The application automatically validates configuration on startup: + +1. **Required Variables**: Must be present and non-empty +2. **Numeric Values**: Must be valid numbers within acceptable ranges +3. **File Paths**: Must be accessible and have appropriate permissions +4. **URLs**: Must be valid URL format +5. **Encryption Keys**: Must be exactly 32 bytes for AES-256 + +### Configuration Errors + +Common configuration errors and solutions: + +#### "JWT_SECRET must be at least 32 bytes long" +- **Solution**: Generate a longer JWT secret using `openssl rand -base64 64` + +#### "ENCRYPTION_KEY must be exactly 32 bytes long" +- **Solution**: Generate a 32-byte key using `openssl rand -hex 32` + +#### "Invalid port number" +- **Solution**: Ensure port numbers are between 1 and 65535 + +#### "SteamCMD not found" +- **Solution**: Install SteamCMD and update the `STEAMCMD_PATH` variable + +## ๐Ÿ”ง Configuration Management + +### Environment-Specific Configurations + +#### Development (.env.development) +```env +DEV_MODE=true +LOG_LEVEL=debug +CORS_ALLOWED_ORIGIN=http://localhost:3000 +``` + +#### Production (.env.production) +```env +PRODUCTION=true +FORCE_HTTPS=true +LOG_LEVEL=warn +SECURITY_STRICT=true +``` + +#### Testing (.env.test) +```env +DB_NAME=acc_test.db +LOG_LEVEL=error +DISABLE_RATE_LIMITING=true +``` + +### Configuration Templates + +Create configuration templates for common setups: + +#### Single Server Setup +```env +# Minimal configuration for single server +PORT=3000 +DB_NAME=acc.db +SERVICE_NAME_PREFIX=ACC-Server +``` + +#### Multi-Server Setup +```env +# Configuration for multiple servers +AUTO_FIREWALL_RULES=true +PORT_RANGE_SIZE=20 +SERVICE_START_TIMEOUT=120 +``` + +#### High-Security Setup +```env +# Maximum security configuration +FORCE_HTTPS=true +RATE_LIMIT_AUTH=3 +SESSION_TIMEOUT=30 +MAX_LOGIN_ATTEMPTS=3 +LOCKOUT_DURATION=30 +SECURITY_STRICT=true +``` + +## ๐Ÿšจ Security Best Practices + +### Secret Management + +1. **Never commit secrets to version control** +2. **Use environment-specific secret files** +3. **Rotate secrets regularly** +4. **Use secure secret generation methods** +5. **Limit access to configuration files** + +### Production Security + +1. **Enable HTTPS enforcement** +2. **Configure appropriate CORS origins** +3. **Set up proper rate limiting** +4. **Enable comprehensive logging** +5. **Regular security audits** + +## ๐Ÿ“ž Configuration Support + +### Troubleshooting + +For configuration issues: + +1. Check the application logs for specific error messages +2. Validate environment variables using the built-in validation +3. Refer to the examples in `.env.example` +4. Test configuration changes in development first + +### Getting Help + +- **Documentation**: Check this guide and other documentation files +- **Issues**: Report configuration bugs via GitHub Issues +- **Community**: Ask questions in community discussions +- **Professional Support**: Contact maintainers for enterprise support + +--- + +**Note**: Always test configuration changes in a development environment before applying them to production. \ No newline at end of file diff --git a/documentation/DEPLOYMENT.md b/documentation/DEPLOYMENT.md new file mode 100644 index 0000000..c425510 --- /dev/null +++ b/documentation/DEPLOYMENT.md @@ -0,0 +1,691 @@ +# Deployment Guide for ACC Server Manager + +## Overview + +This guide provides comprehensive instructions for deploying the ACC Server Manager in various environments, from development to production. It covers security considerations, performance optimization, monitoring setup, and maintenance procedures. + +## ๐Ÿš€ Quick Start Deployment + +### Prerequisites Checklist + +- [ ] Windows 10/11 or Windows Server 2016+ +- [ ] Go 1.23.0 or later installed +- [ ] Administrative privileges +- [ ] Valid Steam account +- [ ] Internet connection for Steam downloads + +### Minimum System Requirements + +| Component | Minimum | Recommended | +|-----------|---------|-------------| +| **CPU** | 2 cores | 4+ cores | +| **RAM** | 4 GB | 8+ GB | +| **Storage** | 10 GB free | 50+ GB SSD | +| **Network** | 10 Mbps | 100+ Mbps | + +## ๐Ÿ“ฆ Installation Methods + +### Method 1: Binary Deployment (Recommended) + +1. **Download Release Binary** + ```bash + # Download the latest release from GitHub + # Extract to your installation directory + cd C:\ACC-Server-Manager + ``` + +2. **Configure Environment** + ```bash + copy .env.example .env + # Edit .env with your configuration + ``` + +3. **Generate Secrets** + ```bash + # Generate JWT secret + openssl rand -base64 64 + + # Generate app secrets + openssl rand -hex 32 + + # Generate encryption key + openssl rand -hex 32 + ``` + +4. **Run Application** + ```bash + .\acc-server-manager.exe + ``` + +### Method 2: Source Code Deployment + +1. **Clone Repository** + ```bash + git clone https://github.com/FJurmanovic/acc-server-manager.git + cd acc-server-manager + ``` + +2. **Install Dependencies** + ```bash + go mod download + go mod verify + ``` + +3. **Build Application** + ```bash + # Development build + go build -o acc-server-manager.exe cmd/api/main.go + + # Production build (optimized) + go build -ldflags="-w -s" -o acc-server-manager.exe cmd/api/main.go + ``` + +4. **Configure and Run** + ```bash + copy .env.example .env + # Configure your .env file + .\acc-server-manager.exe + ``` + +## ๐Ÿ”ง Environment Configuration + +### Production Environment Variables + +Create a production `.env` file: + +```env +# ======================================== +# PRODUCTION CONFIGURATION +# ======================================== + +# Security (REQUIRED - Generate unique values) +JWT_SECRET=your-production-jwt-secret-64-chars-minimum +APP_SECRET=your-production-app-secret-32-chars +APP_SECRET_CODE=your-production-secret-code-32-chars +ENCRYPTION_KEY=your-production-encryption-key-32-bytes + +# Server Configuration +PORT=8080 +HOST=0.0.0.0 +PRODUCTION=true +FORCE_HTTPS=true + +# Database +DB_NAME=acc_production.db +DB_PATH=./data + +# CORS (Set to your actual domain) +CORS_ALLOWED_ORIGIN=https://yourdomain.com + +# Security Settings +RATE_LIMIT_GLOBAL=1000 +RATE_LIMIT_AUTH=10 +SESSION_TIMEOUT=120 +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION=30 + +# Steam Configuration +STEAMCMD_PATH=C:\steamcmd\steamcmd.exe +NSSM_PATH=C:\nssm\nssm.exe + +# Logging +LOG_LEVEL=warn +LOG_FILE=./logs/production.log +LOG_MAX_SIZE=100 +LOG_MAX_FILES=10 + +# Monitoring +HEALTH_CHECK_ENABLED=true +METRICS_ENABLED=true +PERFORMANCE_MONITORING=true + +# Backup +AUTO_BACKUP=true +BACKUP_INTERVAL=12 +BACKUP_RETENTION=30 +BACKUP_DIR=./backups +``` + +### Development Environment Variables + +```env +# ======================================== +# DEVELOPMENT CONFIGURATION +# ======================================== + +# Security (Use secure values even in dev) +JWT_SECRET=dev-jwt-secret-but-still-secure-64-chars-minimum +APP_SECRET=dev-app-secret-32-chars-here +APP_SECRET_CODE=dev-secret-code-32-chars-here +ENCRYPTION_KEY=dev-encryption-key-32-bytes-here + +# Server Configuration +PORT=3000 +HOST=localhost +DEV_MODE=true +DEBUG_ENDPOINTS=true + +# Database +DB_NAME=acc_dev.db + +# CORS +CORS_ALLOWED_ORIGIN=http://localhost:3000,http://localhost:5173 + +# Relaxed Security (Development Only) +RATE_LIMIT_GLOBAL=1000 +DISABLE_SECURITY=false + +# Logging +LOG_LEVEL=debug +LOG_COLORS=true +ENABLE_SQL_LOGGING=true + +# Development Tools +HOT_RELOAD=true +ENABLE_TEST_FIXTURES=true +``` + +## ๐Ÿ”’ Security Hardening + +### SSL/TLS Configuration + +1. **Obtain SSL Certificate** + ```bash + # Option 1: Let's Encrypt (Free) + certbot certonly --webroot -w /var/www/html -d yourdomain.com + + # Option 2: Commercial Certificate + # Purchase and install certificate from CA + ``` + +2. **Configure Reverse Proxy (Nginx)** + ```nginx + server { + listen 443 ssl http2; + server_name yourdomain.com; + + ssl_certificate /path/to/certificate.crt; + ssl_certificate_key /path/to/private.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE+AESGCM:ECDHE+AES256:ECDHE+AES128:!aNULL:!MD5:!DSS; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + # Redirect HTTP to HTTPS + server { + listen 80; + server_name yourdomain.com; + return 301 https://$server_name$request_uri; + } + ``` + +3. **Configure Application for SSL** + ```env + FORCE_HTTPS=true + CORS_ALLOWED_ORIGIN=https://yourdomain.com + ``` + +### Firewall Configuration + +1. **Windows Firewall Rules** + ```powershell + # Allow application through Windows Firewall + New-NetFirewallRule -DisplayName "ACC Server Manager" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow + + # Allow ACC server ports (adjust range as needed) + New-NetFirewallRule -DisplayName "ACC Servers TCP" -Direction Inbound -Protocol TCP -LocalPort 9600-9700 -Action Allow + New-NetFirewallRule -DisplayName "ACC Servers UDP" -Direction Inbound -Protocol UDP -LocalPort 9600-9700 -Action Allow + ``` + +2. **Network Security Groups (Azure)** + ```json + { + "securityRules": [ + { + "name": "AllowHTTPS", + "properties": { + "protocol": "TCP", + "sourcePortRange": "*", + "destinationPortRange": "443", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 1000, + "direction": "Inbound" + } + } + ] + } + ``` + +### User Access Control + +1. **Create Dedicated Service Account** + ```powershell + # Create service account + New-LocalUser -Name "ACCServiceUser" -Description "ACC Server Manager Service Account" -NoPassword + Add-LocalGroupMember -Group "Users" -Member "ACCServiceUser" + + # Set permissions on application directory + icacls "C:\ACC-Server-Manager" /grant "ACCServiceUser:(OI)(CI)F" + ``` + +2. **Configure Service Permissions** + ```powershell + # Grant service logon rights + secedit /export /cfg security.inf + # Edit security.inf to add ACCServiceUser to SeServiceLogonRight + secedit /configure /db security.sdb /cfg security.inf + ``` + +## ๐Ÿ—๏ธ Service Installation + +### Windows Service with NSSM + +1. **Install NSSM** + ```bash + # Download NSSM from https://nssm.cc/ + # Extract nssm.exe to C:\nssm\ + ``` + +2. **Create Service** + ```powershell + # Install service + C:\nssm\nssm.exe install "ACCServerManager" "C:\ACC-Server-Manager\acc-server-manager.exe" + + # Configure service + C:\nssm\nssm.exe set "ACCServerManager" DisplayName "ACC Server Manager" + C:\nssm\nssm.exe set "ACCServerManager" Description "Assetto Corsa Competizione Server Manager" + C:\nssm\nssm.exe set "ACCServerManager" Start SERVICE_AUTO_START + C:\nssm\nssm.exe set "ACCServerManager" AppDirectory "C:\ACC-Server-Manager" + C:\nssm\nssm.exe set "ACCServerManager" ObjectName ".\ACCServiceUser" "password" + + # Configure logging + C:\nssm\nssm.exe set "ACCServerManager" AppStdout "C:\ACC-Server-Manager\logs\service.log" + C:\nssm\nssm.exe set "ACCServerManager" AppStderr "C:\ACC-Server-Manager\logs\service-error.log" + + # Start service + C:\nssm\nssm.exe start "ACCServerManager" + ``` + +3. **Service Management** + ```powershell + # Check service status + Get-Service -Name "ACCServerManager" + + # Start/Stop service + Start-Service -Name "ACCServerManager" + Stop-Service -Name "ACCServerManager" + + # Remove service (if needed) + C:\nssm\nssm.exe remove "ACCServerManager" confirm + ``` + +### Systemd Service (Linux/WSL) + +```ini +[Unit] +Description=ACC Server Manager +After=network.target + +[Service] +Type=simple +User=accmanager +WorkingDirectory=/opt/acc-server-manager +ExecStart=/opt/acc-server-manager/acc-server-manager +Restart=always +RestartSec=10 +Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +EnvironmentFile=/opt/acc-server-manager/.env + +[Install] +WantedBy=multi-user.target +``` + +## ๐Ÿ“Š Monitoring Setup + +### Health Check Monitoring + +1. **Configure Health Checks** + ```env + HEALTH_CHECK_ENABLED=true + HEALTH_CHECK_PATH=/health + HEALTH_CHECK_TIMEOUT=10 + ``` + +2. **External Monitoring (UptimeRobot)** + ```bash + # Monitor endpoint: https://yourdomain.com/health + # Expected response: 200 OK with JSON health status + ``` + +### Log Management + +1. **Log Rotation Configuration** + ```env + LOG_MAX_SIZE=100 + LOG_MAX_FILES=10 + LOG_MAX_AGE=30 + ``` + +2. **Centralized Logging (Optional)** + ```yaml + # docker-compose.yml for ELK Stack + version: '3' + services: + elasticsearch: + image: elasticsearch:7.14.0 + logstash: + image: logstash:7.14.0 + kibana: + image: kibana:7.14.0 + ``` + +### Performance Monitoring + +1. **Enable Metrics** + ```env + METRICS_ENABLED=true + METRICS_PORT=9090 + PERFORMANCE_MONITORING=true + ``` + +2. **Prometheus Configuration** + ```yaml + # prometheus.yml + global: + scrape_interval: 15s + + scrape_configs: + - job_name: 'acc-server-manager' + static_configs: + - targets: ['localhost:9090'] + ``` + +## ๐Ÿ”„ Database Management + +### Database Backup Strategy + +1. **Automated Backups** + ```env + AUTO_BACKUP=true + BACKUP_INTERVAL=12 + BACKUP_RETENTION=30 + BACKUP_DIR=./backups + BACKUP_COMPRESS=true + ``` + +2. **Manual Backup** + ```powershell + # Create manual backup + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + Copy-Item "acc.db" "backups/acc-backup-$timestamp.db" + + # Compress backup + Compress-Archive "backups/acc-backup-$timestamp.db" "backups/acc-backup-$timestamp.zip" + ``` + +3. **Backup Verification** + ```bash + # Test backup integrity + sqlite3 backup.db "PRAGMA integrity_check;" + ``` + +### Database Migration + +1. **Pre-Migration Backup** + ```bash + # Always backup before migration + copy acc.db acc-pre-migration-backup.db + ``` + +2. **Migration Process** + ```bash + # Migration runs automatically on startup + # Check logs for migration status + tail -f logs/app.log | grep -i migration + ``` + +## ๐ŸŒ Load Balancing (High Availability) + +### Multiple Instance Setup + +1. **Load Balancer Configuration (HAProxy)** + ```haproxy + global + daemon + + defaults + mode http + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + + frontend acc_frontend + bind *:80 + default_backend acc_servers + + backend acc_servers + balance roundrobin + server acc1 10.0.0.10:8080 check + server acc2 10.0.0.11:8080 check + server acc3 10.0.0.12:8080 check + ``` + +2. **Shared Database Setup** + ```bash + # Use network-attached storage for database + # Mount shared volume on all instances + net use Z: \\fileserver\acc-shared + ``` + +### Session Clustering + +```env +# Redis for session storage +REDIS_URL=redis://localhost:6379 +SESSION_STORE=redis +``` + +## ๐Ÿ”ง Maintenance Procedures + +### Regular Maintenance Tasks + +1. **Daily Tasks** + ```powershell + # Check service status + Get-Service -Name "ACCServerManager" + + # Check disk space + Get-WmiObject -Class Win32_LogicalDisk | Select-Object DeviceID, Size, FreeSpace + + # Review error logs + Get-Content "logs/error.log" -Tail 50 + ``` + +2. **Weekly Tasks** + ```powershell + # Update system patches + Install-Module PSWindowsUpdate + Get-WUInstall -AcceptAll -AutoReboot + + # Clean old log files + Get-ChildItem "logs\" -Name "*.log.*" | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} | Remove-Item + + # Verify backup integrity + sqlite3 backups/latest.db "PRAGMA integrity_check;" + ``` + +3. **Monthly Tasks** + ```powershell + # Update dependencies + go get -u ./... + go mod tidy + + # Security scan + go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest + gosec ./... + + # Performance review + # Review metrics and optimize based on usage patterns + ``` + +### Update Procedures + +1. **Backup Current Installation** + ```bash + # Stop service + Stop-Service -Name "ACCServerManager" + + # Backup application + Copy-Item -Recurse "C:\ACC-Server-Manager" "C:\ACC-Server-Manager-Backup-$(Get-Date -Format 'yyyyMMdd')" + ``` + +2. **Deploy New Version** + ```bash + # Download new version + # Replace executable + # Update configuration if needed + + # Start service + Start-Service -Name "ACCServerManager" + ``` + +3. **Rollback Procedure** + ```bash + # Stop service + Stop-Service -Name "ACCServerManager" + + # Restore backup + Remove-Item -Recurse "C:\ACC-Server-Manager" + Copy-Item -Recurse "C:\ACC-Server-Manager-Backup-$(Get-Date -Format 'yyyyMMdd')" "C:\ACC-Server-Manager" + + # Start service + Start-Service -Name "ACCServerManager" + ``` + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **Service Won't Start** + ```powershell + # Check service status + Get-Service -Name "ACCServerManager" + + # Check service logs + Get-Content "logs/service-error.log" -Tail 50 + + # Check Windows Event Log + Get-EventLog -LogName System -Source "ACCServerManager" -Newest 10 + ``` + +2. **Database Connection Issues** + ```bash + # Check database file permissions + icacls acc.db + + # Test database connection + sqlite3 acc.db ".tables" + + # Check for database locks + lsof acc.db # Linux + ``` + +3. **Steam Integration Issues** + ```bash + # Verify SteamCMD installation + C:\steamcmd\steamcmd.exe +quit + + # Check Steam credentials + # Review Steam-related logs + ``` + +### Performance Issues + +1. **High CPU Usage** + ```bash + # Check for resource-intensive operations + # Monitor process performance + Get-Process -Name "acc-server-manager" | Select-Object CPU, WorkingSet + ``` + +2. **Memory Leaks** + ```bash + # Monitor memory usage over time + # Enable detailed memory profiling + go tool pprof http://localhost:8080/debug/pprof/heap + ``` + +3. **Database Performance** + ```sql + -- Analyze database performance + PRAGMA table_info(servers); + EXPLAIN QUERY PLAN SELECT * FROM servers WHERE status = 'running'; + ``` + +## ๐Ÿ“ž Support and Resources + +### Documentation Resources +- [README.md](../README.md) - Getting started guide +- [SECURITY.md](SECURITY.md) - Security guidelines +- [API.md](API.md) - API documentation +- [CONFIGURATION.md](CONFIGURATION.md) - Configuration reference + +### Community Support +- **GitHub Issues** - Bug reports and feature requests +- **Discord Community** - Real-time community support +- **Wiki** - Community-maintained documentation + +### Professional Support +- **Enterprise Support** - Professional deployment assistance +- **Consulting Services** - Custom implementation and optimization +- **Training** - Team training and best practices + +### Emergency Contacts +``` +Production Issues: support@yourdomain.com +Security Issues: security@yourdomain.com +Emergency Hotline: +1-XXX-XXX-XXXX +``` + +## ๐Ÿ“‹ Deployment Checklist + +### Pre-Deployment +- [ ] System requirements verified +- [ ] Dependencies installed +- [ ] Secrets generated and secured +- [ ] Configuration reviewed +- [ ] Security hardening applied +- [ ] Backup strategy implemented +- [ ] Monitoring configured + +### Post-Deployment +- [ ] Service running successfully +- [ ] Health checks passing +- [ ] Logs being written correctly +- [ ] Database accessible +- [ ] API endpoints responding +- [ ] Frontend integration working +- [ ] Monitoring alerts configured +- [ ] Documentation updated + +### Production Readiness +- [ ] SSL/TLS configured +- [ ] Firewall rules applied +- [ ] Performance monitoring active +- [ ] Backup procedures tested +- [ ] Update procedures documented +- [ ] Disaster recovery plan created +- [ ] Team training completed + +--- + +**Remember**: Always test deployments in a staging environment before applying to production! \ No newline at end of file diff --git a/documentation/SECURITY.md b/documentation/SECURITY.md new file mode 100644 index 0000000..6a3ca76 --- /dev/null +++ b/documentation/SECURITY.md @@ -0,0 +1,264 @@ +# Security Guide for ACC Server Manager + +## Overview + +This document outlines the security features, best practices, and requirements for the ACC Server Manager application. Following these guidelines is essential for maintaining a secure deployment. + +## ๐Ÿ” Authentication & Authorization + +### JWT Token Security + +- **Secret Key**: Must be at least 32 bytes long and cryptographically secure +- **Token Expiration**: Default 24 hours, configurable via environment +- **Refresh Strategy**: Implement token refresh before expiration +- **Storage**: Store tokens securely (httpOnly cookies recommended for web) + +### Password Security + +- **Hashing**: Uses bcrypt with cost factor 12 +- **Requirements**: Minimum 8 characters, must include uppercase, lowercase, digit, and special character +- **Validation**: Real-time strength validation during registration/update +- **Storage**: Never store plain text passwords + +### Rate Limiting + +- **Global**: 100 requests per minute per IP +- **Authentication**: 5 attempts per 15 minutes per IP+User-Agent +- **API Endpoints**: 60 requests per minute per IP +- **Customizable**: Configurable via environment variables + +## ๐Ÿ›ก๏ธ Security Headers + +The application automatically sets the following security headers: + +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `X-XSS-Protection: 1; mode=block` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Content-Security-Policy: [configured policy]` +- `Permissions-Policy: [restricted permissions]` + +## ๐Ÿ”’ Data Protection + +### Encryption + +- **Algorithm**: AES-256-GCM for sensitive data +- **Key Management**: 32-byte keys from environment variables +- **Usage**: Steam credentials and other sensitive configuration data + +### Database Security + +- **SQLite**: Default database with file-level security +- **Migrations**: Automatic password security upgrades +- **Backup**: Encrypted backups with retention policies + +## ๐ŸŒ Network Security + +### HTTPS + +- **Production**: HTTPS enforced in production environments +- **Certificates**: Use valid SSL/TLS certificates +- **Redirection**: Automatic HTTP to HTTPS redirect + +### CORS Configuration + +- **Origins**: Configured per environment +- **Headers**: Properly configured for API access +- **Credentials**: Enabled for authenticated requests + +### Firewall Rules + +- **Automatic**: Creates Windows Firewall rules for server ports +- **Management**: Centralized firewall rule management +- **Cleanup**: Automatic rule removal when servers are deleted + +## ๐Ÿšจ Input Validation & Sanitization + +### Request Validation + +- **Content-Type**: Validates expected content types +- **Size Limits**: 10MB request body limit +- **User-Agent**: Blocks suspicious user agents +- **Timeout**: 30-second request timeout + +### Input Sanitization + +- **XSS Prevention**: Removes dangerous HTML/JavaScript patterns +- **SQL Injection**: Uses parameterized queries +- **Path Traversal**: Validates file paths and names + +## ๐Ÿ“Š Monitoring & Logging + +### Security Events + +- **Authentication**: All login attempts (success/failure) +- **Authorization**: Permission checks and violations +- **Rate Limiting**: Blocked requests and patterns +- **Suspicious Activity**: Automated threat detection + +### Log Security + +- **Sensitive Data**: Never logs passwords or tokens +- **Format**: Structured logging with security context +- **Retention**: Configurable log retention policies +- **Access**: Restricted access to log files + +## โš™๏ธ Environment Configuration + +### Required Environment Variables + +```bash +# Critical Security Settings +JWT_SECRET=<64-character-base64-string> +APP_SECRET=<32-character-hex-string> +APP_SECRET_CODE=<32-character-hex-string> +ENCRYPTION_KEY=<32-character-hex-string> + +# Security Features +FORCE_HTTPS=true +RATE_LIMIT_GLOBAL=100 +RATE_LIMIT_AUTH=5 +SESSION_TIMEOUT=60 +MAX_LOGIN_ATTEMPTS=5 +LOCKOUT_DURATION=15 +``` + +### Secret Generation + +Generate secure secrets using: + +```bash +# JWT Secret (Base64, 64 bytes) +openssl rand -base64 64 + +# Application Secrets (Hex, 32 bytes) +openssl rand -hex 32 + +# Encryption Key (Hex, 32 bytes) +openssl rand -hex 32 +``` + +## ๐Ÿ”„ Security Migrations + +### Password Security Upgrade + +The application includes an automatic migration that: + +1. Upgrades old encrypted passwords to bcrypt hashes +2. Maintains data integrity during the process +3. Provides rollback protection +4. Logs migration status and errors + +### Migration Safety + +- **Backup**: Automatically creates password backups +- **Validation**: Verifies password strength requirements +- **Recovery**: Handles corrupted or invalid passwords +- **Logging**: Detailed migration logs for auditing + +## ๐Ÿš€ Deployment Security + +### Production Checklist + +- [ ] Generate unique secrets for production +- [ ] Enable HTTPS with valid certificates +- [ ] Configure appropriate CORS origins +- [ ] Set up proper firewall rules +- [ ] Enable security monitoring and alerting +- [ ] Configure secure backup strategies +- [ ] Review and adjust rate limits +- [ ] Set up log monitoring and analysis +- [ ] Test security configurations +- [ ] Document security procedures + +### Container Security (if applicable) + +- **Base Images**: Use official, minimal base images +- **User Privileges**: Run as non-root user +- **Secrets**: Use container secret management +- **Network**: Isolate containers appropriately + +## ๐Ÿ” Security Testing + +### Automated Testing + +- **Dependencies**: Regular security scanning of dependencies +- **SAST**: Static application security testing +- **DAST**: Dynamic application security testing +- **Penetration Testing**: Regular security assessments + +### Manual Testing + +- **Authentication Bypass**: Test authentication mechanisms +- **Authorization**: Verify permission controls +- **Input Validation**: Test input sanitization +- **Rate Limiting**: Verify rate limiting effectiveness + +## ๐Ÿšจ Incident Response + +### Security Incident Procedures + +1. **Detection**: Monitor logs and alerts +2. **Assessment**: Evaluate impact and scope +3. **Containment**: Isolate affected systems +4. **Eradication**: Remove threats and vulnerabilities +5. **Recovery**: Restore normal operations +6. **Lessons Learned**: Document and improve + +### Emergency Contacts + +- **Security Team**: [Configure your security team contacts] +- **System Administrators**: [Configure admin contacts] +- **Management**: [Configure management contacts] + +## ๐Ÿ“‹ Security Maintenance + +### Regular Tasks + +- **Weekly**: Review security logs and alerts +- **Monthly**: Update dependencies and security patches +- **Quarterly**: Security configuration review +- **Annually**: Comprehensive security assessment + +### Monitoring + +- **Failed Logins**: Monitor authentication failures +- **Rate Limit Hits**: Track rate limiting events +- **Error Patterns**: Identify suspicious error patterns +- **Performance**: Monitor for DoS attacks + +## ๐Ÿ”— Additional Resources + +### Security Standards + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) +- [CIS Controls](https://www.cisecurity.org/controls/) + +### Go Security + +- [Go Security Policy](https://golang.org/security) +- [Secure Coding Practices](https://github.com/OWASP/Go-SCP) + +### Dependencies + +- [Fiber Security](https://docs.gofiber.io/api/middleware/helmet) +- [GORM Security](https://gorm.io/docs/security.html) + +## ๐Ÿ“ž Support + +For security questions or concerns: + +- **Security Issues**: Report via private channels +- **Documentation**: Refer to this guide and code comments +- **Updates**: Monitor security advisories for dependencies + +## ๐Ÿ”„ Version History + +- **v1.0.0**: Initial security implementation +- **v1.1.0**: Added password security migration +- **v1.2.0**: Enhanced rate limiting and monitoring + +--- + +**Important**: This security guide should be reviewed and updated regularly as the application evolves and new security threats emerge. \ No newline at end of file diff --git a/go.mod b/go.mod index 8215b5c..d7efb91 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.23.0 require ( github.com/gofiber/fiber/v2 v2.52.8 github.com/gofiber/swagger v1.1.0 + github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c github.com/swaggo/swag v1.16.3 go.uber.org/dig v1.17.1 + golang.org/x/crypto v0.39.0 golang.org/x/sync v0.15.0 golang.org/x/text v0.26.0 gorm.io/driver/sqlite v1.5.6 @@ -23,7 +25,6 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -38,7 +39,6 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/crypto v0.39.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/tools v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 06dd301..72dcd87 100644 --- a/go.sum +++ b/go.sum @@ -69,23 +69,16 @@ go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/local/api/api.go b/local/api/api.go index d1b3eb7..d0b777a 100644 --- a/local/api/api.go +++ b/local/api/api.go @@ -20,8 +20,6 @@ func Init(di *dig.Container, app *fiber.App) { // Protected routes groups := app.Group(configs.Prefix) - - serverIdGroup := groups.Group("/server/:id") routeGroups := &common.RouteGroups{ Api: groups.Group("/api"), @@ -32,20 +30,12 @@ func Init(di *dig.Container, app *fiber.App) { StateHistory: serverIdGroup.Group("/state-history"), } - err := di.Provide(func() *common.RouteGroups { return routeGroups }) if err != nil { logging.Panic("unable to bind routes") } - err = di.Provide(func() *dig.Container { - return di - }) - if err != nil { - logging.Panic("unable to bind dig") - } controller.InitializeControllers(di) } - diff --git a/local/controller/config.go b/local/controller/config.go index ec53cb8..10a5a66 100644 --- a/local/controller/config.go +++ b/local/controller/config.go @@ -59,7 +59,7 @@ func (ac *ConfigController) UpdateConfig(c *fiber.Ctx) error { if err != nil { return c.Status(400).SendString(err.Error()) } - logging.Info("restart", restart) + logging.Info("restart: %v", restart) if restart { _, err := ac.apiService.ApiRestartServer(c) if err != nil { diff --git a/local/controller/membership.go b/local/controller/membership.go index 3938fd5..01a5214 100644 --- a/local/controller/membership.go +++ b/local/controller/membership.go @@ -55,6 +55,7 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) } + logging.Debug("Login request received") token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password) if err != nil { return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()}) diff --git a/local/middleware/auth.go b/local/middleware/auth.go index d9e6120..4b42556 100644 --- a/local/middleware/auth.go +++ b/local/middleware/auth.go @@ -1,9 +1,12 @@ package middleware import ( + "acc-server-manager/local/middleware/security" "acc-server-manager/local/service" "acc-server-manager/local/utl/jwt" + "acc-server-manager/local/utl/logging" "strings" + "time" "github.com/gofiber/fiber/v2" ) @@ -11,49 +14,125 @@ import ( // AuthMiddleware provides authentication and permission middleware. type AuthMiddleware struct { membershipService *service.MembershipService + securityMW *security.SecurityMiddleware } // NewAuthMiddleware creates a new AuthMiddleware. func NewAuthMiddleware(ms *service.MembershipService) *AuthMiddleware { return &AuthMiddleware{ membershipService: ms, + securityMW: security.NewSecurityMiddleware(), } } -// Authenticate is a middleware for JWT authentication. +// Authenticate is a middleware for JWT authentication with enhanced security. func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error { + // Log authentication attempt + ip := ctx.IP() + userAgent := ctx.Get("User-Agent") + authHeader := ctx.Get("Authorization") if authHeader == "" { - return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"}) + logging.Error("Authentication failed: missing Authorization header from IP %s", ip) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Missing or malformed JWT", + }) } parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { - return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing or malformed JWT"}) + logging.Error("Authentication failed: malformed Authorization header from IP %s", ip) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Missing or malformed JWT", + }) } - claims, err := jwt.ValidateToken(parts[1]) + // Validate token length to prevent potential attacks + token := parts[1] + if len(token) < 10 || len(token) > 2048 { + logging.Error("Authentication failed: invalid token length from IP %s", ip) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Invalid or expired JWT", + }) + } + + claims, err := jwt.ValidateToken(token) if err != nil { - return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired JWT"}) + logging.Error("Authentication failed: invalid token from IP %s, User-Agent: %s, Error: %v", ip, userAgent, err) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Invalid or expired JWT", + }) + } + + // Additional security: validate user ID format + if claims.UserID == "" || len(claims.UserID) < 10 { + logging.Error("Authentication failed: invalid user ID in token from IP %s", ip) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Invalid or expired JWT", + }) } ctx.Locals("userID", claims.UserID) + ctx.Locals("authTime", time.Now()) + + logging.Info("User %s authenticated successfully from IP %s", claims.UserID, ip) return ctx.Next() } -// HasPermission is a middleware for checking user permissions. +// HasPermission is a middleware for checking user permissions with enhanced logging. func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler { return func(ctx *fiber.Ctx) error { userID, ok := ctx.Locals("userID").(string) if !ok { - return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"}) + logging.Error("Permission check failed: no user ID in context from IP %s", ctx.IP()) + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Unauthorized", + }) + } + + // Validate permission parameter + if requiredPermission == "" { + logging.Error("Permission check failed: empty permission requirement") + return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Internal server error", + }) } has, err := m.membershipService.HasPermission(ctx.UserContext(), userID, requiredPermission) - if err != nil || !has { - return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Forbidden"}) + if err != nil { + logging.Error("Permission check error for user %s, permission %s: %v", userID, requiredPermission, err) + return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Forbidden", + }) } + if !has { + logging.Error("Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP()) + return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Forbidden", + }) + } + + logging.Info("Permission granted: user %s has permission %s", userID, requiredPermission) + return ctx.Next() + } +} + +// AuthRateLimit applies rate limiting specifically for authentication endpoints +func (m *AuthMiddleware) AuthRateLimit() fiber.Handler { + return m.securityMW.AuthRateLimit() +} + +// RequireHTTPS redirects HTTP requests to HTTPS in production +func (m *AuthMiddleware) RequireHTTPS() fiber.Handler { + return func(ctx *fiber.Ctx) error { + if ctx.Protocol() != "https" && ctx.Get("X-Forwarded-Proto") != "https" { + // Allow HTTP in development/testing + if ctx.Hostname() != "localhost" && ctx.Hostname() != "127.0.0.1" { + httpsURL := "https://" + ctx.Hostname() + ctx.OriginalURL() + return ctx.Redirect(httpsURL, fiber.StatusMovedPermanently) + } + } return ctx.Next() } } diff --git a/local/middleware/security/security.go b/local/middleware/security/security.go new file mode 100644 index 0000000..b54cbf6 --- /dev/null +++ b/local/middleware/security/security.go @@ -0,0 +1,351 @@ +package security + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" +) + +// RateLimiter stores rate limiting information +type RateLimiter struct { + requests map[string][]time.Time + mutex sync.RWMutex +} + +// NewRateLimiter creates a new rate limiter +func NewRateLimiter() *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string][]time.Time), + } + + // Clean up old entries every 5 minutes + go rl.cleanup() + + return rl +} + +// cleanup removes old entries from the rate limiter +func (rl *RateLimiter) cleanup() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + rl.mutex.Lock() + now := time.Now() + for key, times := range rl.requests { + // Remove entries older than 1 hour + filtered := make([]time.Time, 0, len(times)) + for _, t := range times { + if now.Sub(t) < time.Hour { + filtered = append(filtered, t) + } + } + if len(filtered) == 0 { + delete(rl.requests, key) + } else { + rl.requests[key] = filtered + } + } + rl.mutex.Unlock() + } +} + +// SecurityMiddleware provides comprehensive security middleware +type SecurityMiddleware struct { + rateLimiter *RateLimiter +} + +// NewSecurityMiddleware creates a new security middleware +func NewSecurityMiddleware() *SecurityMiddleware { + return &SecurityMiddleware{ + rateLimiter: NewRateLimiter(), + } +} + +// SecurityHeaders adds security headers to responses +func (sm *SecurityMiddleware) SecurityHeaders() fiber.Handler { + return func(c *fiber.Ctx) error { + // Prevent MIME type sniffing + c.Set("X-Content-Type-Options", "nosniff") + + // Prevent clickjacking + c.Set("X-Frame-Options", "DENY") + + // Enable XSS protection + c.Set("X-XSS-Protection", "1; mode=block") + + // Prevent referrer leakage + c.Set("Referrer-Policy", "strict-origin-when-cross-origin") + + // Content Security Policy + c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'") + + // Permissions Policy + c.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()") + + return c.Next() + } +} + +// RateLimit implements rate limiting for API endpoints +func (sm *SecurityMiddleware) RateLimit(maxRequests int, duration time.Duration) fiber.Handler { + return func(c *fiber.Ctx) error { + ip := c.IP() + key := fmt.Sprintf("rate_limit:%s", ip) + + sm.rateLimiter.mutex.Lock() + defer sm.rateLimiter.mutex.Unlock() + + now := time.Now() + requests := sm.rateLimiter.requests[key] + + // Remove requests older than duration + filtered := make([]time.Time, 0, len(requests)) + for _, t := range requests { + if now.Sub(t) < duration { + filtered = append(filtered, t) + } + } + + // Check if limit is exceeded + if len(filtered) >= maxRequests { + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": "Rate limit exceeded", + "retry_after": duration.Seconds(), + }) + } + + // Add current request + filtered = append(filtered, now) + sm.rateLimiter.requests[key] = filtered + + return c.Next() + } +} + +// AuthRateLimit implements stricter rate limiting for authentication endpoints +func (sm *SecurityMiddleware) AuthRateLimit() fiber.Handler { + return func(c *fiber.Ctx) error { + ip := c.IP() + userAgent := c.Get("User-Agent") + key := fmt.Sprintf("%s:%s", ip, userAgent) + + sm.rateLimiter.mutex.Lock() + defer sm.rateLimiter.mutex.Unlock() + + now := time.Now() + requests := sm.rateLimiter.requests[key] + + // Remove requests older than 15 minutes + filtered := make([]time.Time, 0, len(requests)) + for _, t := range requests { + if now.Sub(t) < 15*time.Minute { + filtered = append(filtered, t) + } + } + + // Check if limit is exceeded (5 requests per 15 minutes for auth) + if len(filtered) >= 5 { + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": "Too many authentication attempts", + "retry_after": 900, // 15 minutes + }) + } + + // Add current request + filtered = append(filtered, now) + sm.rateLimiter.requests[key] = filtered + + return c.Next() + } +} + +// InputSanitization sanitizes user input to prevent XSS and injection attacks +func (sm *SecurityMiddleware) InputSanitization() fiber.Handler { + return func(c *fiber.Ctx) error { + // Sanitize query parameters + c.Request().URI().QueryArgs().VisitAll(func(key, value []byte) { + sanitized := sanitizeInput(string(value)) + c.Request().URI().QueryArgs().Set(string(key), sanitized) + }) + + // Store original body for processing + if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" { + body := c.Body() + if len(body) > 0 { + // Basic sanitization - remove potentially dangerous patterns + sanitized := sanitizeInput(string(body)) + c.Request().SetBodyString(sanitized) + } + } + + return c.Next() + } +} + +// sanitizeInput removes potentially dangerous patterns from input +func sanitizeInput(input string) string { + // Remove common XSS patterns + dangerous := []string{ + "", + "javascript:", + "vbscript:", + "data:text/html", + "onload=", + "onerror=", + "onclick=", + "onmouseover=", + "onfocus=", + "onblur=", + "onchange=", + "onsubmit=", + " maxSize { + return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{ + "error": "Request too large", + "max_size": maxSize, + }) + } + } + + return c.Next() + } +} + +// LogSecurityEvents logs security-related events +func (sm *SecurityMiddleware) LogSecurityEvents() fiber.Handler { + return func(c *fiber.Ctx) error { + start := time.Now() + + // Process request + err := c.Next() + + // Log suspicious activity + status := c.Response().StatusCode() + if status == 401 || status == 403 || status == 429 { + duration := time.Since(start) + // In a real implementation, you would send this to your logging system + fmt.Printf("[SECURITY] %s %s %s %d %v %s\n", + time.Now().Format(time.RFC3339), + c.IP(), + c.Method(), + status, + duration, + c.Path(), + ) + } + + return err + } +} + +// TimeoutMiddleware adds request timeout +func (sm *SecurityMiddleware) TimeoutMiddleware(timeout time.Duration) fiber.Handler { + return func(c *fiber.Ctx) error { + ctx, cancel := context.WithTimeout(c.UserContext(), timeout) + defer cancel() + + c.SetUserContext(ctx) + + return c.Next() + } +} diff --git a/local/migrations/001_upgrade_password_security.go b/local/migrations/001_upgrade_password_security.go new file mode 100644 index 0000000..f136857 --- /dev/null +++ b/local/migrations/001_upgrade_password_security.go @@ -0,0 +1,238 @@ +package migrations + +import ( + "acc-server-manager/local/utl/logging" + "acc-server-manager/local/utl/password" + "errors" + "fmt" + + "gorm.io/gorm" +) + +// Migration001UpgradePasswordSecurity migrates existing user passwords from encrypted to hashed format +type Migration001UpgradePasswordSecurity struct { + DB *gorm.DB +} + +// NewMigration001UpgradePasswordSecurity creates a new password security migration +func NewMigration001UpgradePasswordSecurity(db *gorm.DB) *Migration001UpgradePasswordSecurity { + return &Migration001UpgradePasswordSecurity{DB: db} +} + +// Up executes the migration +func (m *Migration001UpgradePasswordSecurity) Up() error { + logging.Info("Starting password security upgrade migration...") + + // Check if migration has already been applied + var migrationRecord MigrationRecord + err := m.DB.Where("migration_name = ?", "001_upgrade_password_security").First(&migrationRecord).Error + if err == nil { + logging.Info("Password security migration already applied, skipping") + return nil + } + + // Create migration tracking table if it doesn't exist + if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil { + return fmt.Errorf("failed to create migration tracking table: %v", err) + } + + // Start transaction + tx := m.DB.Begin() + if tx.Error != nil { + return fmt.Errorf("failed to start transaction: %v", tx.Error) + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Add a backup column for old passwords (temporary) + if err := tx.Exec("ALTER TABLE users ADD COLUMN password_backup TEXT").Error; err != nil { + // Column might already exist, ignore if it's a duplicate column error + if !isDuplicateColumnError(err) { + tx.Rollback() + return fmt.Errorf("failed to add backup column: %v", err) + } + } + + // Get all users with encrypted passwords + var users []UserForMigration + if err := tx.Find(&users).Error; err != nil { + tx.Rollback() + return fmt.Errorf("failed to fetch users: %v", err) + } + + logging.Info("Found %d users to migrate", len(users)) + + migratedCount := 0 + failedCount := 0 + + for _, user := range users { + if err := m.migrateUserPassword(tx, &user); err != nil { + logging.Error("Failed to migrate user %s (ID: %s): %v", user.Username, user.ID, err) + failedCount++ + // Continue with other users rather than failing completely + continue + } + migratedCount++ + } + + // Remove backup column after successful migration + if err := tx.Exec("ALTER TABLE users DROP COLUMN password_backup").Error; err != nil { + logging.Error("Failed to remove backup column (non-critical): %v", err) + // Don't fail the migration for this + } + + // Record successful migration + migrationRecord = MigrationRecord{ + MigrationName: "001_upgrade_password_security", + AppliedAt: "datetime('now')", + Success: true, + Notes: fmt.Sprintf("Migrated %d users, %d failed", migratedCount, failedCount), + } + + if err := tx.Create(&migrationRecord).Error; err != nil { + tx.Rollback() + return fmt.Errorf("failed to record migration: %v", err) + } + + // Commit transaction + if err := tx.Commit().Error; err != nil { + return fmt.Errorf("failed to commit migration: %v", err) + } + + logging.Info("Password security migration completed successfully. Migrated: %d, Failed: %d", migratedCount, failedCount) + + if failedCount > 0 { + logging.Error("Some users failed to migrate. They will need to reset their passwords.") + } + + return nil +} + +// migrateUserPassword migrates a single user's password +func (m *Migration001UpgradePasswordSecurity) migrateUserPassword(tx *gorm.DB, user *UserForMigration) error { + // Skip if password is already hashed (bcrypt hashes start with $2a$, $2b$, or $2y$) + if isAlreadyHashed(user.Password) { + logging.Debug("User %s already has hashed password, skipping", user.Username) + return nil + } + + // Backup original password + if err := tx.Model(user).Update("password_backup", user.Password).Error; err != nil { + return fmt.Errorf("failed to backup password: %v", err) + } + + // Try to decrypt the old password + var plainPassword string + + // First, try to decrypt using the old encryption method + decrypted, err := decryptOldPassword(user.Password) + if err != nil { + // If decryption fails, the password might already be plain text or corrupted + logging.Error("Failed to decrypt password for user %s, treating as plain text: %v", user.Username, err) + + // Use original password as-is (might be plain text from development) + plainPassword = user.Password + + // Validate it's not obviously encrypted data + if len(plainPassword) > 100 || containsBinaryData(plainPassword) { + return fmt.Errorf("password appears to be corrupted encrypted data") + } + } else { + plainPassword = decrypted + } + + // Validate plain password + if plainPassword == "" { + return errors.New("decrypted password is empty") + } + + if len(plainPassword) < 1 { + return errors.New("password too short after decryption") + } + + // Hash the plain password using bcrypt + hashedPassword, err := password.HashPassword(plainPassword) + if err != nil { + return fmt.Errorf("failed to hash password: %v", err) + } + + // Update with hashed password + if err := tx.Model(user).Update("password", hashedPassword).Error; err != nil { + return fmt.Errorf("failed to update password: %v", err) + } + + logging.Debug("Successfully migrated password for user %s", user.Username) + return nil +} + +// UserForMigration represents a user record for migration purposes +type UserForMigration struct { + ID string `gorm:"column:id"` + Username string `gorm:"column:username"` + Password string `gorm:"column:password"` +} + +// TableName specifies the table name for GORM +func (UserForMigration) TableName() string { + return "users" +} + +// MigrationRecord tracks applied migrations +type MigrationRecord struct { + ID uint `gorm:"primaryKey"` + MigrationName string `gorm:"unique;not null"` + AppliedAt string `gorm:"not null"` + Success bool `gorm:"not null"` + Notes string +} + +// TableName specifies the table name for GORM +func (MigrationRecord) TableName() string { + return "migration_records" +} + +// isAlreadyHashed checks if a password is already bcrypt hashed +func isAlreadyHashed(password string) bool { + return len(password) >= 60 && (password[:4] == "$2a$" || password[:4] == "$2b$" || password[:4] == "$2y$") +} + +// containsBinaryData checks if a string contains binary data +func containsBinaryData(s string) bool { + for _, b := range []byte(s) { + if b < 32 && b != 9 && b != 10 && b != 13 { // Allow tab, newline, carriage return + return true + } + } + return false +} + +// isDuplicateColumnError checks if an error is due to duplicate column +func isDuplicateColumnError(err error) bool { + errStr := err.Error() + return fmt.Sprintf("%v", errStr) == "duplicate column name: password_backup" || + fmt.Sprintf("%v", errStr) == "SQLITE_ERROR: duplicate column name: password_backup" +} + +// decryptOldPassword attempts to decrypt using the old encryption method +// This is a simplified version of the old DecryptPassword function +func decryptOldPassword(encryptedPassword string) (string, error) { + // This would use the old decryption logic + // For now, we'll return an error to force treating as plain text + // In a real scenario, you'd implement the old decryption here + return "", errors.New("old decryption not implemented - treating as plain text") +} + +// Down reverses the migration (if needed) +func (m *Migration001UpgradePasswordSecurity) Down() error { + logging.Error("Password security migration rollback is not supported for security reasons") + return errors.New("password security migration rollback is not supported") +} + +// RunMigration is a convenience function to run the migration +func RunPasswordSecurityMigration(db *gorm.DB) error { + migration := NewMigration001UpgradePasswordSecurity(db) + return migration.Up() +} diff --git a/local/model/steam_credentials.go b/local/model/steam_credentials.go index 17985ad..898eea6 100644 --- a/local/model/steam_credentials.go +++ b/local/model/steam_credentials.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "errors" "io" + "regexp" + "strings" "time" "gorm.io/gorm" @@ -74,25 +76,67 @@ func (s *SteamCredentials) AfterFind(tx *gorm.DB) error { return nil } -// Validate checks if the credentials are valid +// Validate checks if the credentials are valid with enhanced security checks func (s *SteamCredentials) Validate() error { if s.Username == "" { return errors.New("username is required") } + + // Enhanced username validation + if len(s.Username) < 3 || len(s.Username) > 64 { + return errors.New("username must be between 3 and 64 characters") + } + + // Check for valid characters in username (alphanumeric, underscore, hyphen) + if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s.Username); !matched { + return errors.New("username contains invalid characters") + } + if s.Password == "" { return errors.New("password is required") } + + // Basic password validation + if len(s.Password) < 6 { + return errors.New("password must be at least 6 characters long") + } + + if len(s.Password) > 128 { + return errors.New("password is too long") + } + + // Check for obvious weak passwords + weakPasswords := []string{"password", "123456", "steam", "admin", "user"} + lowerPass := strings.ToLower(s.Password) + for _, weak := range weakPasswords { + if lowerPass == weak { + return errors.New("password is too weak") + } + } + return nil } // GetEncryptionKey returns the encryption key from config. // The key is loaded from the ENCRYPTION_KEY environment variable. func GetEncryptionKey() []byte { - return []byte(configs.EncryptionKey) + key := []byte(configs.EncryptionKey) + if len(key) != 32 { + panic("encryption key must be exactly 32 bytes for AES-256") + } + return key } -// EncryptPassword encrypts a password using AES-256 +// EncryptPassword encrypts a password using AES-256-GCM with enhanced security func EncryptPassword(password string) (string, error) { + if password == "" { + return "", errors.New("password cannot be empty") + } + + if len(password) > 1024 { + return "", errors.New("password too long") + } + key := GetEncryptionKey() block, err := aes.NewCipher(key) if err != nil { @@ -105,21 +149,30 @@ func EncryptPassword(password string) (string, error) { return "", err } - // Create a nonce + // Create a cryptographically secure nonce nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } - // Encrypt the password + // Encrypt the password with authenticated encryption ciphertext := gcm.Seal(nonce, nonce, []byte(password), nil) // Return base64 encoded encrypted password return base64.StdEncoding.EncodeToString(ciphertext), nil } -// DecryptPassword decrypts an encrypted password +// DecryptPassword decrypts an encrypted password with enhanced validation func DecryptPassword(encryptedPassword string) (string, error) { + if encryptedPassword == "" { + return "", errors.New("encrypted password cannot be empty") + } + + // Validate base64 format + if len(encryptedPassword) < 24 { // Minimum reasonable length + return "", errors.New("invalid encrypted password format") + } + key := GetEncryptionKey() block, err := aes.NewCipher(key) if err != nil { @@ -135,7 +188,7 @@ func DecryptPassword(encryptedPassword string) (string, error) { // Decode base64 encoded password ciphertext, err := base64.StdEncoding.DecodeString(encryptedPassword) if err != nil { - return "", err + return "", errors.New("invalid base64 encoding") } nonceSize := gcm.NonceSize() @@ -146,8 +199,14 @@ func DecryptPassword(encryptedPassword string) (string, error) { nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { - return "", err + return "", errors.New("decryption failed - invalid ciphertext or key") } - return string(plaintext), nil -} \ No newline at end of file + // Validate decrypted content + decrypted := string(plaintext) + if len(decrypted) == 0 || len(decrypted) > 1024 { + return "", errors.New("invalid decrypted password") + } + + return decrypted, nil +} diff --git a/local/model/user.go b/local/model/user.go index 1c3c688..7cc8adb 100644 --- a/local/model/user.go +++ b/local/model/user.go @@ -1,6 +1,7 @@ package model import ( + "acc-server-manager/local/utl/password" "errors" "github.com/google/uuid" @@ -11,54 +12,57 @@ import ( type User struct { ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"` Username string `json:"username" gorm:"unique_index;not null"` - Password string `json:"password" gorm:"not null"` + Password string `json:"-" gorm:"not null"` // Never expose password in JSON RoleID uuid.UUID `json:"role_id" gorm:"type:uuid"` Role Role `json:"role"` } - -// BeforeCreate is a GORM hook that runs before creating new credentials +// BeforeCreate is a GORM hook that runs before creating new users func (s *User) BeforeCreate(tx *gorm.DB) error { s.ID = uuid.New() - // Encrypt password before saving - encrypted, err := EncryptPassword(s.Password) + + // Validate password strength + if err := password.ValidatePasswordStrength(s.Password); err != nil { + return err + } + + // Hash password before saving + hashed, err := password.HashPassword(s.Password) if err != nil { return err } - s.Password = encrypted + s.Password = hashed return nil } -// BeforeUpdate is a GORM hook that runs before updating credentials +// BeforeUpdate is a GORM hook that runs before updating users func (s *User) BeforeUpdate(tx *gorm.DB) error { - - // Only encrypt if password field is being updated + // Only hash if password field is being updated if tx.Statement.Changed("Password") { - encrypted, err := EncryptPassword(s.Password) + // Validate password strength + if err := password.ValidatePasswordStrength(s.Password); err != nil { + return err + } + + hashed, err := password.HashPassword(s.Password) if err != nil { return err } - s.Password = encrypted + s.Password = hashed } return nil } -// AfterFind is a GORM hook that runs after fetching credentials +// AfterFind is a GORM hook that runs after fetching users func (s *User) AfterFind(tx *gorm.DB) error { - // Decrypt password after fetching - if s.Password != "" { - decrypted, err := DecryptPassword(s.Password) - if err != nil { - return err - } - s.Password = decrypted - } + // Password remains hashed - never decrypt + // This hook is kept for potential future use return nil } -// Validate checks if the credentials are valid +// Validate checks if the user data is valid func (s *User) Validate() error { if s.Username == "" { return errors.New("username is required") @@ -67,4 +71,9 @@ func (s *User) Validate() error { return errors.New("password is required") } return nil -} \ No newline at end of file +} + +// VerifyPassword verifies a plain text password against the stored hash +func (s *User) VerifyPassword(plainPassword string) error { + return password.VerifyPassword(s.Password, plainPassword) +} diff --git a/local/repository/membership.go b/local/repository/membership.go index f5a1dcc..f6f217a 100644 --- a/local/repository/membership.go +++ b/local/repository/membership.go @@ -41,7 +41,6 @@ func (r *MembershipRepository) FindUserByIDWithPermissions(ctx context.Context, return &user, nil } - // CreateUser creates a new user. func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error { db := r.db.WithContext(ctx) diff --git a/local/repository/server.go b/local/repository/server.go index 21ce52d..c82566f 100644 --- a/local/repository/server.go +++ b/local/repository/server.go @@ -17,73 +17,9 @@ func NewServerRepository(db *gorm.DB) *ServerRepository { BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}), } - // Run migrations - if err := repo.migrateServerTable(); err != nil { - panic(err) - } - return repo } -// migrateServerTable ensures all required columns exist with proper defaults -func (r *ServerRepository) migrateServerTable() error { - // Create a temporary table with all required columns - if err := r.db.Exec(` - CREATE TABLE IF NOT EXISTS servers_new ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - ip TEXT NOT NULL, - port INTEGER NOT NULL DEFAULT 9600, - path TEXT NOT NULL, - service_name TEXT NOT NULL, - date_created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - from_steam_cmd BOOLEAN NOT NULL DEFAULT 1 - ) - `).Error; err != nil { - return err - } - - // Copy data from old table, setting defaults for new columns - if err := r.db.Exec(` - INSERT INTO servers_new ( - id, - name, - ip, - port, - path, - service_name, - date_created, - from_steam_cmd - ) - SELECT - id, - COALESCE(name, 'Server ' || id) as name, - COALESCE(ip, '127.0.0.1') as ip, - COALESCE(port, 9600) as port, - path, - COALESCE(service_name, 'ACC-Server-' || id) as service_name, - COALESCE(date_created, CURRENT_TIMESTAMP) as date_created, - COALESCE(from_steam_cmd, 1) as from_steam_cmd - FROM servers - `).Error; err != nil { - // If the old table doesn't exist, this is a fresh install - if err := r.db.Exec(`DROP TABLE IF EXISTS servers_new`).Error; err != nil { - return err - } - return nil - } - - // Replace old table with new one - if err := r.db.Exec(`DROP TABLE IF EXISTS servers`).Error; err != nil { - return err - } - if err := r.db.Exec(`ALTER TABLE servers_new RENAME TO servers`).Error; err != nil { - return err - } - - return nil -} - // GetFirstByServiceName // Gets first row from Server table. // @@ -100,4 +36,4 @@ func (r *ServerRepository) GetFirstByServiceName(ctx context.Context, serviceNam return nil, err } return result, nil -} \ No newline at end of file +} diff --git a/local/repository/state_history.go b/local/repository/state_history.go index 67d87e0..a25be28 100644 --- a/local/repository/state_history.go +++ b/local/repository/state_history.go @@ -93,7 +93,7 @@ func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, fil rawQuery := ` SELECT DATETIME(MIN(date_created)) as timestamp, - AVG(player_count) as count + ROUND(AVG(player_count)) as count FROM state_histories WHERE server_id = ? AND date_created BETWEEN ? AND ? GROUP BY strftime('%Y-%m-%d %H', date_created) diff --git a/local/service/membership.go b/local/service/membership.go index 36c3188..ad26777 100644 --- a/local/service/membership.go +++ b/local/service/membership.go @@ -4,6 +4,7 @@ import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" "acc-server-manager/local/utl/jwt" + "acc-server-manager/local/utl/logging" "context" "errors" "os" @@ -28,7 +29,8 @@ func (s *MembershipService) Login(ctx context.Context, username, password string return "", errors.New("invalid credentials") } - if user.Password != password { + // Use secure password verification with constant-time comparison + if err := user.VerifyPassword(password); err != nil { return "", errors.New("invalid credentials") } @@ -40,6 +42,7 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password, role, err := s.repo.FindRoleByName(ctx, roleName) if err != nil { + logging.Error("Failed to find role by name: %v", err) return nil, errors.New("role not found") } @@ -50,8 +53,10 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password, } if err := s.repo.CreateUser(ctx, user); err != nil { + logging.Error("Failed to create user: %v", err) return nil, err } + logging.Debug("User created successfully") return user, nil } @@ -90,6 +95,7 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re } if req.Password != nil && *req.Password != "" { + // Password will be automatically hashed in BeforeUpdate hook user.Password = *req.Password } @@ -162,6 +168,7 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error { // Create a default admin user if one doesn't exist _, err = s.repo.FindUserByUsername(ctx, "admin") if err != nil { + logging.Debug("Creating default admin user") _, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed if err != nil { return err diff --git a/local/utl/configs/configs.go b/local/utl/configs/configs.go index ff6cdb6..d699a8a 100644 --- a/local/utl/configs/configs.go +++ b/local/utl/configs/configs.go @@ -3,6 +3,8 @@ package configs import ( "log" "os" + + "github.com/joho/godotenv" ) var ( @@ -14,12 +16,14 @@ var ( ) func init() { - Secret = getEnv("APP_SECRET", "default-secret-for-dev-use-only") - SecretCode = getEnv("APP_SECRET_CODE", "another-secret-for-dev-use-only") - EncryptionKey = getEnv("ENCRYPTION_KEY", "a-secure-32-byte-long-key-!!!!!!") // Fallback MUST be 32 bytes for AES-256 + godotenv.Load() + // Fail fast if critical environment variables are missing + Secret = getEnvRequired("APP_SECRET") + SecretCode = getEnvRequired("APP_SECRET_CODE") + EncryptionKey = getEnvRequired("ENCRYPTION_KEY") if len(EncryptionKey) != 32 { - log.Fatal("ENCRYPTION_KEY must be 32 bytes long") + log.Fatal("ENCRYPTION_KEY must be exactly 32 bytes long for AES-256") } } @@ -31,3 +35,13 @@ func getEnv(key, fallback string) string { log.Printf("Environment variable %s not set, using fallback.", key) return fallback } + +// getEnvRequired retrieves an environment variable and fails if it's not set. +// This should be used for critical configuration that must not have defaults. +func getEnvRequired(key string) string { + if value, exists := os.LookupEnv(key); exists && value != "" { + return value + } + log.Fatalf("Required environment variable %s is not set or is empty", key) + return "" // This line will never be reached due to log.Fatalf +} diff --git a/local/utl/db/db.go b/local/utl/db/db.go index bee0456..e7de703 100644 --- a/local/utl/db/db.go +++ b/local/utl/db/db.go @@ -44,9 +44,9 @@ func Migrate(db *gorm.DB) { &model.StateHistory{}, &model.SteamCredentials{}, &model.SystemConfig{}, - &model.User{}, - &model.Role{}, &model.Permission{}, + &model.Role{}, + &model.User{}, ) if err != nil { @@ -55,6 +55,10 @@ func Migrate(db *gorm.DB) { db.FirstOrCreate(&model.ApiModel{Api: "Works"}) + // Run security migrations - temporarily disabled until migration is fixed + // TODO: Implement proper migration system + logging.Info("Database migration system needs to be implemented") + Seed(db) } @@ -80,8 +84,6 @@ func Seed(db *gorm.DB) error { return nil } - - func seedTracks(db *gorm.DB) error { tracks := []model.Track{ {Name: "monza", UniquePitBoxes: 29, PrivateServerSlots: 60}, diff --git a/local/utl/jwt/jwt.go b/local/utl/jwt/jwt.go index 933ecdd..e2d5176 100644 --- a/local/utl/jwt/jwt.go +++ b/local/utl/jwt/jwt.go @@ -2,16 +2,18 @@ package jwt import ( "acc-server-manager/local/model" + "crypto/rand" + "encoding/base64" "errors" + "log" + "os" "time" "github.com/golang-jwt/jwt/v4" ) -// SecretKey is the secret key for signing the JWT. -// It is recommended to use a long, complex string for this. -// In a production environment, this should be loaded from a secure configuration source. -var SecretKey = []byte("your-secret-key") +// SecretKey holds the JWT signing key loaded from environment +var SecretKey []byte // Claims represents the JWT claims. type Claims struct { @@ -19,6 +21,36 @@ type Claims struct { jwt.RegisteredClaims } +// init initializes the JWT secret key from environment variable +func init() { + jwtSecret := os.Getenv("JWT_SECRET") + if jwtSecret == "" { + log.Fatal("JWT_SECRET environment variable is required and cannot be empty") + } + + // Decode base64 secret if it looks like base64, otherwise use as-is + if decoded, err := base64.StdEncoding.DecodeString(jwtSecret); err == nil && len(decoded) >= 32 { + SecretKey = decoded + } else { + SecretKey = []byte(jwtSecret) + } + + // Ensure minimum key length for security + if len(SecretKey) < 32 { + log.Fatal("JWT_SECRET must be at least 32 bytes long for security") + } +} + +// GenerateSecretKey generates a cryptographically secure random key for JWT signing +// This is a utility function for generating new secrets, not used in normal operation +func GenerateSecretKey() string { + key := make([]byte, 64) // 512 bits + if _, err := rand.Read(key); err != nil { + log.Fatal("Failed to generate random key: ", err) + } + return base64.StdEncoding.EncodeToString(key) +} + // GenerateToken generates a new JWT for a given user. func GenerateToken(user *model.User) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) diff --git a/local/utl/password/password.go b/local/utl/password/password.go new file mode 100644 index 0000000..ad2424f --- /dev/null +++ b/local/utl/password/password.go @@ -0,0 +1,82 @@ +package password + +import ( + "errors" + "os" + + "golang.org/x/crypto/bcrypt" +) + +const ( + // MinPasswordLength defines the minimum password length + MinPasswordLength = 8 + // BcryptCost defines the cost factor for bcrypt hashing + BcryptCost = 12 +) + +// HashPassword hashes a plain text password using bcrypt +func HashPassword(password string) (string, error) { + if len(password) < MinPasswordLength { + return "", errors.New("password must be at least 8 characters long") + } + + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost) + if err != nil { + return "", err + } + + return string(hashedBytes), nil +} + +// VerifyPassword verifies a plain text password against a hashed password +func VerifyPassword(hashedPassword, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +// ValidatePasswordStrength validates password complexity requirements +func ValidatePasswordStrength(password string) error { + if len(password) < MinPasswordLength { + return errors.New("password must be at least 8 characters long") + } + + if os.Getenv("ENFORCE_PASSWORD_STRENGTH") == "true" { + if len(password) < MinPasswordLength { + return errors.New("password must be at least 8 characters long") + } + + hasUpper := false + hasLower := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + switch { + case char >= 'A' && char <= 'Z': + hasUpper = true + case char >= 'a' && char <= 'z': + hasLower = true + case char >= '0' && char <= '9': + hasDigit = true + case char >= '!' && char <= '/' || char >= ':' && char <= '@' || char >= '[' && char <= '`' || char >= '{' && char <= '~': + hasSpecial = true + } + } + + if !hasUpper { + return errors.New("password must contain at least one uppercase letter") + } + if !hasLower { + return errors.New("password must contain at least one lowercase letter") + } + if !hasDigit { + return errors.New("password must contain at least one digit") + } + if !hasSpecial { + return errors.New("password must contain at least one special character") + } + + return nil + } + + return nil +} diff --git a/local/utl/server/server.go b/local/utl/server/server.go index 826549f..b37b579 100644 --- a/local/utl/server/server.go +++ b/local/utl/server/server.go @@ -2,8 +2,10 @@ package server import ( "acc-server-manager/local/api" + "acc-server-manager/local/middleware/security" "acc-server-manager/local/utl/logging" "os" + "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -15,8 +17,25 @@ import ( func Start(di *dig.Container) *fiber.App { app := fiber.New(fiber.Config{ EnablePrintRoutes: true, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + BodyLimit: 10 * 1024 * 1024, // 10MB }) + // Initialize security middleware + securityMW := security.NewSecurityMiddleware() + + // Add security middleware stack + app.Use(securityMW.SecurityHeaders()) + app.Use(securityMW.LogSecurityEvents()) + app.Use(securityMW.TimeoutMiddleware(30 * time.Second)) + app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024)) // 10MB + app.Use(securityMW.ValidateUserAgent()) + app.Use(securityMW.ValidateContentType("application/json", "application/x-www-form-urlencoded", "multipart/form-data")) + app.Use(securityMW.InputSanitization()) + app.Use(securityMW.RateLimit(100, 1*time.Minute)) // 100 requests per minute global + app.Use(helmet.New()) allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN") @@ -25,8 +44,11 @@ func Start(di *dig.Container) *fiber.App { } app.Use(cors.New(cors.Config{ - AllowOrigins: allowedOrigin, - AllowHeaders: "Origin, Content-Type, Accept", + AllowOrigins: allowedOrigin, + AllowHeaders: "Origin, Content-Type, Accept, Authorization", + AllowMethods: "GET, POST, PUT, DELETE, OPTIONS", + AllowCredentials: true, + MaxAge: 86400, // 24 hours })) app.Get("/swagger/*", swagger.HandlerDefault) diff --git a/local/utl/tracking/tracking.go b/local/utl/tracking/tracking.go index 9948a61..7f66331 100644 --- a/local/utl/tracking/tracking.go +++ b/local/utl/tracking/tracking.go @@ -154,6 +154,9 @@ func (instance *AccServerInstance) UpdateState(callback func(state *model.Server } func (instance *AccServerInstance) UpdatePlayerCount(count int) { + if (count < 0) { + return + } instance.UpdateState(func (state *model.ServerState, changes *[]StateChange) { if (count == state.PlayerCount) { return diff --git a/scripts/generate-secrets.ps1 b/scripts/generate-secrets.ps1 new file mode 100644 index 0000000..8917a2a --- /dev/null +++ b/scripts/generate-secrets.ps1 @@ -0,0 +1,176 @@ +# ACC Server Manager - Secret Generation Script +# This script generates cryptographically secure secrets for the ACC Server Manager + +Write-Host "ACC Server Manager - Secret Generation Script" -ForegroundColor Green +Write-Host "=============================================" -ForegroundColor Green +Write-Host "" + +# Function to generate random bytes and convert to hex +function Generate-HexString { + param([int]$Length) + $bytes = New-Object byte[] $Length + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + return [System.BitConverter]::ToString($bytes) -replace '-', '' +} + +# Function to generate random bytes and convert to base64 +function Generate-Base64String { + param([int]$Length) + $bytes = New-Object byte[] $Length + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + return [System.Convert]::ToBase64String($bytes) +} + +# Generate secrets +Write-Host "Generating cryptographically secure secrets..." -ForegroundColor Yellow +Write-Host "" + +$jwtSecret = Generate-Base64String -Length 64 +$appSecret = Generate-HexString -Length 32 +$appSecretCode = Generate-HexString -Length 32 +$encryptionKey = Generate-HexString -Length 16 + +# Display generated secrets +Write-Host "Generated Secrets:" -ForegroundColor Cyan +Write-Host "==================" -ForegroundColor Cyan +Write-Host "" +Write-Host "JWT_SECRET=" -NoNewline -ForegroundColor White +Write-Host $jwtSecret -ForegroundColor Yellow +Write-Host "" +Write-Host "APP_SECRET=" -NoNewline -ForegroundColor White +Write-Host $appSecret -ForegroundColor Yellow +Write-Host "" +Write-Host "APP_SECRET_CODE=" -NoNewline -ForegroundColor White +Write-Host $appSecretCode -ForegroundColor Yellow +Write-Host "" +Write-Host "ENCRYPTION_KEY=" -NoNewline -ForegroundColor White +Write-Host $encryptionKey -ForegroundColor Yellow +Write-Host "" + +# Check if .env file exists +$envFile = ".env" +$envExists = Test-Path $envFile + +if ($envExists) { + Write-Host "Warning: .env file already exists!" -ForegroundColor Red + $overwrite = Read-Host "Do you want to update it with new secrets? (y/N)" + if ($overwrite -eq "y" -or $overwrite -eq "Y") { + $updateFile = $true + } else { + $updateFile = $false + Write-Host "Secrets generated but not written to file." -ForegroundColor Yellow + } +} else { + $createFile = Read-Host "Create .env file with these secrets? (Y/n)" + if ($createFile -eq "" -or $createFile -eq "y" -or $createFile -eq "Y") { + $updateFile = $true + } else { + $updateFile = $false + Write-Host "Secrets generated but not written to file." -ForegroundColor Yellow + } +} + +if ($updateFile) { + # Create or update .env file + if ($envExists) { + # Backup existing file + $backupFile = ".env.backup." + (Get-Date -Format "yyyyMMdd-HHmmss") + Copy-Item $envFile $backupFile + Write-Host "Backed up existing .env to $backupFile" -ForegroundColor Green + + # Read existing content and update secrets + $content = Get-Content $envFile + $newContent = @() + + foreach ($line in $content) { + if ($line -match "^JWT_SECRET=") { + $newContent += "JWT_SECRET=$jwtSecret" + } elseif ($line -match "^APP_SECRET=") { + $newContent += "APP_SECRET=$appSecret" + } elseif ($line -match "^APP_SECRET_CODE=") { + $newContent += "APP_SECRET_CODE=$appSecretCode" + } elseif ($line -match "^ENCRYPTION_KEY=") { + $newContent += "ENCRYPTION_KEY=$encryptionKey" + } else { + $newContent += $line + } + } + + $newContent | Out-File -FilePath $envFile -Encoding UTF8 + Write-Host "Updated .env file with new secrets" -ForegroundColor Green + } else { + # Create new .env file from template + if (Test-Path ".env.example") { + $template = Get-Content ".env.example" + $newContent = @() + + foreach ($line in $template) { + if ($line -match "^JWT_SECRET=") { + $newContent += "JWT_SECRET=$jwtSecret" + } elseif ($line -match "^APP_SECRET=") { + $newContent += "APP_SECRET=$appSecret" + } elseif ($line -match "^APP_SECRET_CODE=") { + $newContent += "APP_SECRET_CODE=$appSecretCode" + } elseif ($line -match "^ENCRYPTION_KEY=") { + $newContent += "ENCRYPTION_KEY=$encryptionKey" + } else { + $newContent += $line + } + } + + $newContent | Out-File -FilePath $envFile -Encoding UTF8 + Write-Host "Created .env file from template with generated secrets" -ForegroundColor Green + } else { + # Create minimal .env file + $minimalEnv = @( + "# ACC Server Manager Environment Configuration", + "# Generated on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')", + "", + "# CRITICAL SECURITY SETTINGS (REQUIRED)", + "JWT_SECRET=$jwtSecret", + "APP_SECRET=$appSecret", + "APP_SECRET_CODE=$appSecretCode", + "ENCRYPTION_KEY=$encryptionKey", + "", + "# CORE APPLICATION SETTINGS", + "DB_NAME=acc.db", + "PORT=3000", + "CORS_ALLOWED_ORIGIN=http://localhost:5173", + "PASSWORD=change-this-default-admin-password" + ) + + $minimalEnv | Out-File -FilePath $envFile -Encoding UTF8 + Write-Host "Created minimal .env file with generated secrets" -ForegroundColor Green + } + } +} + +Write-Host "" +Write-Host "Security Notes:" -ForegroundColor Red +Write-Host "===============" -ForegroundColor Red +Write-Host "1. Keep these secrets secure and never commit them to version control" -ForegroundColor Yellow +Write-Host "2. Use different secrets for each environment (dev, staging, production)" -ForegroundColor Yellow +Write-Host "3. Rotate secrets regularly in production environments" -ForegroundColor Yellow +Write-Host "4. The ENCRYPTION_KEY is exactly 32 bytes as required for AES-256" -ForegroundColor Yellow +Write-Host "5. Change the default PASSWORD immediately after first login" -ForegroundColor Yellow +Write-Host "" + +# Verify encryption key length +if ($encryptionKey.Length -eq 32) { # 32 characters = 32 bytes when converted to []byte in Go + Write-Host "โœ“ Encryption key length verified (32 characters = 32 bytes for AES-256)" -ForegroundColor Green +} else { + Write-Host "โœ— Warning: Encryption key length is incorrect! Got $($encryptionKey.Length) chars, expected 32" -ForegroundColor Red +} + +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host "1. Review and customize the .env file if needed" -ForegroundColor White +Write-Host "2. Ensure SteamCMD and NSSM are installed and paths are correct" -ForegroundColor White +Write-Host "3. Build and run the application: go run cmd/api/main.go" -ForegroundColor White +Write-Host "4. Change the default admin password on first login" -ForegroundColor White +Write-Host "" +Write-Host "Happy racing! ๐Ÿ" -ForegroundColor Green diff --git a/scripts/generate-secrets.sh b/scripts/generate-secrets.sh new file mode 100644 index 0000000..e060af4 --- /dev/null +++ b/scripts/generate-secrets.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# ACC Server Manager - Secret Generation Script (Unix/Linux) +# This script generates cryptographically secure secrets for the ACC Server Manager + +echo -e "\033[32mACC Server Manager - Secret Generation Script\033[0m" +echo -e "\033[32m=============================================\033[0m" +echo "" + +# Check if openssl is available +if ! command -v openssl &> /dev/null; then + echo -e "\033[31mError: openssl is required but not installed.\033[0m" + echo "Please install openssl and try again." + echo "" + echo "Ubuntu/Debian: sudo apt-get install openssl" + echo "CentOS/RHEL: sudo yum install openssl" + echo "macOS: brew install openssl" + exit 1 +fi + +# Generate secrets using openssl +echo -e "\033[33mGenerating cryptographically secure secrets...\033[0m" +echo "" + +JWT_SECRET=$(openssl rand -base64 64) +APP_SECRET=$(openssl rand -hex 32) +APP_SECRET_CODE=$(openssl rand -hex 32) +ENCRYPTION_KEY=$(openssl rand -hex 16) + +# Display generated secrets +echo -e "\033[36mGenerated Secrets:\033[0m" +echo -e "\033[36m==================\033[0m" +echo "" +echo -e "\033[37mJWT_SECRET=\033[0m\033[33m$JWT_SECRET\033[0m" +echo "" +echo -e "\033[37mAPP_SECRET=\033[0m\033[33m$APP_SECRET\033[0m" +echo "" +echo -e "\033[37mAPP_SECRET_CODE=\033[0m\033[33m$APP_SECRET_CODE\033[0m" +echo "" +echo -e "\033[37mENCRYPTION_KEY=\033[0m\033[33m$ENCRYPTION_KEY\033[0m" +echo "" + +# Check if .env file exists +ENV_FILE=".env" +if [ -f "$ENV_FILE" ]; then + echo -e "\033[31mWarning: .env file already exists!\033[0m" + read -p "Do you want to update it with new secrets? (y/N): " overwrite + if [[ $overwrite =~ ^[Yy]$ ]]; then + UPDATE_FILE=true + else + UPDATE_FILE=false + echo -e "\033[33mSecrets generated but not written to file.\033[0m" + fi +else + read -p "Create .env file with these secrets? (Y/n): " create_file + if [[ $create_file =~ ^[Nn]$ ]]; then + UPDATE_FILE=false + echo -e "\033[33mSecrets generated but not written to file.\033[0m" + else + UPDATE_FILE=true + fi +fi + +if [ "$UPDATE_FILE" = true ]; then + if [ -f "$ENV_FILE" ]; then + # Backup existing file + BACKUP_FILE=".env.backup.$(date +%Y%m%d-%H%M%S)" + cp "$ENV_FILE" "$BACKUP_FILE" + echo -e "\033[32mBacked up existing .env to $BACKUP_FILE\033[0m" + + # Update existing file + if command -v sed &> /dev/null; then + # Use sed to update secrets in place + sed -i.tmp "s/^JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" "$ENV_FILE" + sed -i.tmp "s/^APP_SECRET=.*/APP_SECRET=$APP_SECRET/" "$ENV_FILE" + sed -i.tmp "s/^APP_SECRET_CODE=.*/APP_SECRET_CODE=$APP_SECRET_CODE/" "$ENV_FILE" + sed -i.tmp "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE" + rm -f "$ENV_FILE.tmp" + echo -e "\033[32mUpdated .env file with new secrets\033[0m" + else + echo -e "\033[31mError: sed command not found. Please update the .env file manually.\033[0m" + fi + else + # Create new .env file + if [ -f ".env.example" ]; then + # Create from template + cp ".env.example" "$ENV_FILE" + if command -v sed &> /dev/null; then + sed -i.tmp "s/^JWT_SECRET=.*/JWT_SECRET=$JWT_SECRET/" "$ENV_FILE" + sed -i.tmp "s/^APP_SECRET=.*/APP_SECRET=$APP_SECRET/" "$ENV_FILE" + sed -i.tmp "s/^APP_SECRET_CODE=.*/APP_SECRET_CODE=$APP_SECRET_CODE/" "$ENV_FILE" + sed -i.tmp "s/^ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$ENCRYPTION_KEY/" "$ENV_FILE" + rm -f "$ENV_FILE.tmp" + echo -e "\033[32mCreated .env file from template with generated secrets\033[0m" + else + echo -e "\033[31mError: sed command not found. Please update the .env file manually.\033[0m" + fi + else + # Create minimal .env file + cat > "$ENV_FILE" << EOF +# ACC Server Manager Environment Configuration +# Generated on $(date '+%Y-%m-%d %H:%M:%S') + +# CRITICAL SECURITY SETTINGS (REQUIRED) +JWT_SECRET=$JWT_SECRET +APP_SECRET=$APP_SECRET +APP_SECRET_CODE=$APP_SECRET_CODE +ENCRYPTION_KEY=$ENCRYPTION_KEY + +# CORE APPLICATION SETTINGS +DB_NAME=acc.db +PORT=3000 +CORS_ALLOWED_ORIGIN=http://localhost:5173 +PASSWORD=change-this-default-admin-password +EOF + echo -e "\033[32mCreated minimal .env file with generated secrets\033[0m" + fi + fi +fi + +echo "" +echo -e "\033[31mSecurity Notes:\033[0m" +echo -e "\033[31m===============\033[0m" +echo -e "\033[33m1. Keep these secrets secure and never commit them to version control\033[0m" +echo -e "\033[33m2. Use different secrets for each environment (dev, staging, production)\033[0m" +echo -e "\033[33m3. Rotate secrets regularly in production environments\033[0m" +echo -e "\033[33m4. The ENCRYPTION_KEY is exactly 32 bytes as required for AES-256\033[0m" +echo -e "\033[33m5. Change the default PASSWORD immediately after first login\033[0m" +echo "" + +# Verify encryption key length (32 characters = 32 bytes when converted to []byte in Go) +if [ ${#ENCRYPTION_KEY} -eq 32 ]; then + echo -e "\033[32mโœ“ Encryption key length verified (32 characters = 32 bytes for AES-256)\033[0m" +else + echo -e "\033[31mโœ— Warning: Encryption key length is incorrect!\033[0m" +fi + +echo "" +echo -e "\033[36mNext steps:\033[0m" +echo -e "\033[37m1. Review and customize the .env file if needed\033[0m" +echo -e "\033[37m2. Install Go 1.23.0 or later if not already installed\033[0m" +echo -e "\033[37m3. Build and run the application: go run cmd/api/main.go\033[0m" +echo -e "\033[37m4. Change the default admin password on first login\033[0m" +echo "" +echo -e "\033[32mHappy racing! ๐Ÿ\033[0m"