security improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
82
local/utl/password/password.go
Normal file
82
local/utl/password/password.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user