Junie: security cleanups.

This commit is contained in:
Kyle Isom 2025-06-06 13:50:37 -07:00
parent 95d96732d2
commit 23c7a65799
13 changed files with 812 additions and 119 deletions

View File

@ -10,10 +10,10 @@
1. User password authentication. 1. User password authentication.
2. User token authentication. 2. User token authentication.
3. Database credential authentication. 3. Database credential authentication.
4. TOTP (Time-based One-Time Password) authentication.
Future work should consider adding support for: Future work should consider adding support for:
1. TOTP (Time-based One-Time Password) 1. Policy management for fine-grained access control.
2. Policy management for fine-grained access control.
* Documentation * Documentation
@ -29,12 +29,12 @@
1. Initialize the database: 1. Initialize the database:
#+begin_src bash #+begin_src bash
go run main.go init --db ./mcias.db go run cmd/mcias/main.go init --db ./mcias.db
#+end_src #+end_src
2. Start the server: 2. Start the server:
#+begin_src bash #+begin_src bash
go run main.go server --db ./mcias.db go run cmd/mcias/main.go server --db ./mcias.db
#+end_src #+end_src
3. The server will listen on port 8080 by default. 3. The server will listen on port 8080 by default.
@ -52,55 +52,72 @@
Start the MCIAS server: Start the MCIAS server:
#+begin_src bash #+begin_src bash
go run main.go server [--db <path>] [--addr <address>] go run cmd/mcias/main.go server [--db <path>] [--addr <address>]
#+end_src #+end_src
** Init Command ** Init Command
Initialize the database: Initialize the database:
#+begin_src bash #+begin_src bash
go run main.go init [--db <path>] go run cmd/mcias/main.go init [--db <path>]
#+end_src #+end_src
** User Commands ** User Commands
Add a new user: Add a new user:
#+begin_src bash #+begin_src bash
go run main.go user add --username <username> --password <password> go run cmd/mcias/main.go user add --username <username> --password <password>
#+end_src #+end_src
List all users: List all users:
#+begin_src bash #+begin_src bash
go run main.go user list go run cmd/mcias/main.go user list
#+end_src #+end_src
** Token Commands ** Token Commands
Add a new token for a user: Add a new token for a user:
#+begin_src bash #+begin_src bash
go run main.go token add --username <username> [--duration <hours>] go run cmd/mcias/main.go token add --username <username> [--duration <hours>]
#+end_src #+end_src
List all tokens: List all tokens:
#+begin_src bash #+begin_src bash
go run main.go token list go run cmd/mcias/main.go token list
#+end_src
** TOTP Commands
Enable TOTP for a user:
#+begin_src bash
go run cmd/mcias/main.go totp enable --username <username>
#+end_src
Add a TOTP token with QR code generation:
#+begin_src bash
go run cmd/mcias/main.go totp add --username <username> --qr-output <path/to/qrcode.png> [--issuer <issuer>]
#+end_src
Validate a TOTP code:
#+begin_src bash
go run cmd/mcias/main.go totp validate --username <username> --code <totp_code>
#+end_src #+end_src
** Migrate Commands ** Migrate Commands
Apply database migrations: Apply database migrations:
#+begin_src bash #+begin_src bash
go run main.go migrate up [--migrations <dir>] [--steps <n>] go run cmd/mcias/main.go migrate up [--migrations <dir>] [--steps <n>]
#+end_src #+end_src
Revert database migrations: Revert database migrations:
#+begin_src bash #+begin_src bash
go run main.go migrate down [--migrations <dir>] [--steps <n>] go run cmd/mcias/main.go migrate down [--migrations <dir>] [--steps <n>]
#+end_src #+end_src
Show current migration version: Show current migration version:
#+begin_src bash #+begin_src bash
go run main.go migrate version [--migrations <dir>] go run cmd/mcias/main.go migrate version [--migrations <dir>]
#+end_src #+end_src
* API Overview * API Overview

View File

@ -1,14 +1,18 @@
package api package api
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"git.wntrmute.dev/kyle/mcias/data" "git.wntrmute.dev/kyle/mcias/data"
"github.com/Masterminds/squirrel"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
) )
@ -63,48 +67,51 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
return return
} }
// Check password only first // Check password only
if !user.CheckPassword(&req.Login) { if !user.CheckPassword(&req.Login) {
s.sendError(w, "Invalid username or password", http.StatusUnauthorized) s.sendError(w, "Invalid username or password", http.StatusUnauthorized)
return return
} }
// If TOTP is enabled and a code was provided, verify it // Password is correct, create a token regardless of TOTP status
if user.HasTOTP() {
if req.Login.TOTPCode == "" {
// TOTP is enabled but no code was provided
// Return a special response indicating TOTP is required
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
if err := json.NewEncoder(w).Encode(ErrorResponse{
Error: "TOTP code required",
}); err != nil {
s.Logger.Printf("Error encoding response: %v", err)
}
return
}
// Validate the TOTP code
valid, validErr := user.ValidateTOTPCode(req.Login.TOTPCode)
if validErr != nil || !valid {
s.sendError(w, "Invalid TOTP code", http.StatusUnauthorized)
return
}
}
token, expires, err := s.createToken(user.ID) token, expires, err := s.createToken(user.ID)
if err != nil { if err != nil {
s.Logger.Printf("Token creation error: %v", err) s.Logger.Printf("Token creation error: %v", err)
// Log the security event
details := map[string]string{
"reason": "Token creation error",
"error": err.Error(),
}
s.LogSecurityEvent(r, "login_attempt", user.ID, user.User, false, details)
s.sendError(w, "Internal server error", http.StatusInternalServerError) s.sendError(w, "Internal server error", http.StatusInternalServerError)
return return
} }
// If user has TOTP enabled, include this information in the response
totpEnabled := user.HasTOTP()
// Log successful login
details := map[string]string{
"token_expires": time.Unix(expires, 0).UTC().Format(time.RFC3339),
"totp_enabled": fmt.Sprintf("%t", totpEnabled),
}
s.LogSecurityEvent(r, "login_success", user.ID, user.User, true, details)
// Include TOTP status in the response
response := struct {
TokenResponse
TOTPEnabled bool `json:"totp_enabled"`
}{
TokenResponse: TokenResponse{
Token: token,
Expires: expires,
},
TOTPEnabled: totpEnabled,
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(TokenResponse{ if err := json.NewEncoder(w).Encode(response); err != nil {
Token: token,
Expires: expires,
}); err != nil {
s.Logger.Printf("Error encoding response: %v", err) s.Logger.Printf("Error encoding response: %v", err)
} }
} }
@ -147,20 +154,61 @@ func (s *Server) handleTokenLogin(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) sendError(w http.ResponseWriter, message string, status int) { func (s *Server) sendError(w http.ResponseWriter, message string, status int) {
// Log the detailed error message for debugging
s.Logger.Printf("Error response: %s (Status: %d)", message, status)
// Create a generic error message based on status code
publicMessage := "An error occurred processing your request"
errorCode := "E" + string(status)
// Customize public messages for common status codes
// but don't leak specific details about the error
switch status {
case http.StatusBadRequest:
publicMessage = "Invalid request format"
case http.StatusUnauthorized:
publicMessage = "Authentication required"
case http.StatusForbidden:
publicMessage = "Insufficient permissions"
case http.StatusNotFound:
publicMessage = "Resource not found"
case http.StatusTooManyRequests:
publicMessage = "Rate limit exceeded"
case http.StatusInternalServerError:
publicMessage = "Internal server error"
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(ErrorResponse{Error: message}); err != nil {
response := struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}{
Error: publicMessage,
ErrorCode: errorCode,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
s.Logger.Printf("Error encoding error response: %v", err) s.Logger.Printf("Error encoding error response: %v", err)
} }
} }
func (s *Server) getUserByUsername(username string) (*data.User, error) { func (s *Server) getUserByUsername(username string) (*data.User, error) {
query := `SELECT id, created, user, password, salt, totp_secret FROM users WHERE user = ?` // Use squirrel to build the query safely
row := s.DB.QueryRow(query, username) query, args, err := squirrel.Select("id", "created", "user", "password", "salt", "totp_secret").
From("users").
Where(squirrel.Eq{"user": username}).
ToSql()
if err != nil {
return nil, err
}
row := s.DB.QueryRow(query, args...)
user := &data.User{} user := &data.User{}
var totpSecret sql.NullString var totpSecret sql.NullString
err := row.Scan(&user.ID, &user.Created, &user.User, &user.Password, &user.Salt, &totpSecret) err = row.Scan(&user.ID, &user.Created, &user.User, &user.Password, &user.Salt, &totpSecret)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,12 +218,17 @@ func (s *Server) getUserByUsername(username string) (*data.User, error) {
user.TOTPSecret = totpSecret.String user.TOTPSecret = totpSecret.String
} }
rolesQuery := ` // Use squirrel to build the roles query safely
SELECT r.role FROM roles r rolesQuery, rolesArgs, err := squirrel.Select("r.role").
JOIN user_roles ur ON r.id = ur.rid From("roles r").
WHERE ur.uid = ? Join("user_roles ur ON r.id = ur.rid").
` Where(squirrel.Eq{"ur.uid": user.ID}).
rows, err := s.DB.Query(rolesQuery, user.ID) ToSql()
if err != nil {
return nil, err
}
rows, err := s.DB.Query(rolesQuery, rolesArgs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -195,12 +248,28 @@ func (s *Server) getUserByUsername(username string) (*data.User, error) {
} }
func (s *Server) createToken(userID string) (string, int64, error) { func (s *Server) createToken(userID string) (string, int64, error) {
token := ulid.Make().String() // Generate 16 bytes of random data
tokenBytes := make([]byte, 16)
if _, err := rand.Read(tokenBytes); err != nil {
return "", 0, err
}
// Hex encode the random bytes to get a 32-character string
token := hex.EncodeToString(tokenBytes)
expires := time.Now().Add(24 * time.Hour).Unix() expires := time.Now().Add(24 * time.Hour).Unix()
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
tokenID := ulid.Make().String() tokenID := ulid.Make().String()
_, err := s.DB.Exec(query, tokenID, userID, token, expires)
// Use squirrel to build the insert query safely
query, args, err := squirrel.Insert("tokens").
Columns("id", "uid", "token", "expires").
Values(tokenID, userID, token, expires).
ToSql()
if err != nil {
return "", 0, err
}
_, err = s.DB.Exec(query, args...)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
@ -209,14 +278,22 @@ func (s *Server) createToken(userID string) (string, int64, error) {
} }
func (s *Server) verifyToken(username, token string) (string, error) { func (s *Server) verifyToken(username, token string) (string, error) {
query := ` // Use squirrel to build the query safely
SELECT t.uid, t.expires FROM tokens t query, args, err := squirrel.Select("t.uid", "t.expires").
JOIN users u ON t.uid = u.id From("tokens t").
WHERE u.user = ? AND t.token = ? Join("users u ON t.uid = u.id").
` Where(squirrel.And{
squirrel.Eq{"u.user": username},
squirrel.Eq{"t.token": token},
}).
ToSql()
if err != nil {
return "", err
}
var userID string var userID string
var expires int64 var expires int64
err := s.DB.QueryRow(query, username, token).Scan(&userID, &expires) err = s.DB.QueryRow(query, args...).Scan(&userID, &expires)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -230,21 +307,38 @@ func (s *Server) verifyToken(username, token string) (string, error) {
func (s *Server) renewToken(username, token string) (int64, error) { func (s *Server) renewToken(username, token string) (int64, error) {
// First, verify the token exists and get the token ID // First, verify the token exists and get the token ID
query := ` // Use squirrel to build the query safely
SELECT t.id FROM tokens t query, args, err := squirrel.Select("t.id").
JOIN users u ON t.uid = u.id From("tokens t").
WHERE u.user = ? AND t.token = ? Join("users u ON t.uid = u.id").
` Where(squirrel.And{
squirrel.Eq{"u.user": username},
squirrel.Eq{"t.token": token},
}).
ToSql()
if err != nil {
return 0, err
}
var tokenID string var tokenID string
err := s.DB.QueryRow(query, username, token).Scan(&tokenID) err = s.DB.QueryRow(query, args...).Scan(&tokenID)
if err != nil { if err != nil {
return 0, err return 0, err
} }
// Update the token's expiry time // Update the token's expiry time
expires := time.Now().Add(24 * time.Hour).Unix() expires := time.Now().Add(24 * time.Hour).Unix()
updateQuery := `UPDATE tokens SET expires = ? WHERE id = ?`
_, err = s.DB.Exec(updateQuery, expires, tokenID) // Use squirrel to build the update query safely
updateQuery, updateArgs, err := squirrel.Update("tokens").
Set("expires", expires).
Where(squirrel.Eq{"id": tokenID}).
ToSql()
if err != nil {
return 0, err
}
_, err = s.DB.Exec(updateQuery, updateArgs...)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -277,6 +371,11 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
// Check if TOTP is enabled for the user // Check if TOTP is enabled for the user
if !user.HasTOTP() { if !user.HasTOTP() {
// Log the security event
details := map[string]string{
"reason": "TOTP not enabled for user",
}
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
s.sendError(w, "TOTP not enabled for user", http.StatusBadRequest) s.sendError(w, "TOTP not enabled for user", http.StatusBadRequest)
return return
} }
@ -284,6 +383,11 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
// Validate the TOTP code // Validate the TOTP code
valid, validErr := user.ValidateTOTPCode(req.TOTPCode) valid, validErr := user.ValidateTOTPCode(req.TOTPCode)
if validErr != nil || !valid { if validErr != nil || !valid {
// Log the security event
details := map[string]string{
"reason": "Invalid TOTP code",
}
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
s.sendError(w, "Invalid TOTP code", http.StatusUnauthorized) s.sendError(w, "Invalid TOTP code", http.StatusUnauthorized)
return return
} }
@ -292,10 +396,22 @@ func (s *Server) handleTOTPVerify(w http.ResponseWriter, r *http.Request) {
token, expires, err := s.createToken(user.ID) token, expires, err := s.createToken(user.ID)
if err != nil { if err != nil {
s.Logger.Printf("Token creation error: %v", err) s.Logger.Printf("Token creation error: %v", err)
// Log the security event
details := map[string]string{
"reason": "Token creation error",
"error": err.Error(),
}
s.LogSecurityEvent(r, "totp_verification_attempt", user.ID, user.User, false, details)
s.sendError(w, "Internal server error", http.StatusInternalServerError) s.sendError(w, "Internal server error", http.StatusInternalServerError)
return return
} }
// Log successful TOTP verification
details := map[string]string{
"token_expires": time.Unix(expires, 0).UTC().Format(time.RFC3339),
}
s.LogSecurityEvent(r, "totp_verification_success", user.ID, user.User, true, details)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(TokenResponse{ if err := json.NewEncoder(w).Encode(TokenResponse{
@ -356,8 +472,18 @@ func (s *Server) handleDatabaseCredentials(w http.ResponseWriter, r *http.Reques
} }
// Retrieve database credentials // Retrieve database credentials
query := `SELECT id, host, port, name, user, password FROM database LIMIT 1` // Use squirrel to build the query safely
row := s.DB.QueryRow(query) query, args, err := squirrel.Select("id", "host", "port", "name", "user", "password").
From("database").
Limit(1).
ToSql()
if err != nil {
s.Logger.Printf("Query building error: %v", err)
s.sendError(w, "Internal server error", http.StatusInternalServerError)
return
}
row := s.DB.QueryRow(query, args...)
var id string var id string
var creds DatabaseCredentials var creds DatabaseCredentials

View File

@ -2,28 +2,134 @@ package api
import ( import (
"database/sql" "database/sql"
"encoding/json"
"log" "log"
"net"
"net/http" "net/http"
"sync"
"time"
"git.wntrmute.dev/kyle/mcias/data" "git.wntrmute.dev/kyle/mcias/data"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"golang.org/x/time/rate"
) )
// client represents a client with rate limiting information
type client struct {
limiter *rate.Limiter
lastSeen time.Time
}
// SecurityEvent represents a security-related event
type SecurityEvent struct {
Timestamp string `json:"timestamp"`
EventType string `json:"event_type"`
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
RequestURI string `json:"request_uri"`
Success bool `json:"success"`
Details map[string]string `json:"details,omitempty"`
}
// RateLimiter manages rate limiting for clients
type RateLimiter struct {
clients map[string]*client
mu sync.Mutex
// Requests per second, burst size
rate rate.Limit
burst int
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
return &RateLimiter{
clients: make(map[string]*client),
rate: r,
burst: b,
}
}
// GetLimiter returns a rate limiter for a client
func (rl *RateLimiter) GetLimiter(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
c, exists := rl.clients[ip]
if !exists {
c = &client{
limiter: rate.NewLimiter(rl.rate, rl.burst),
lastSeen: time.Now(),
}
rl.clients[ip] = c
} else {
c.lastSeen = time.Now()
}
return c.limiter
}
// CleanupClients removes old clients
func (rl *RateLimiter) CleanupClients() {
rl.mu.Lock()
defer rl.mu.Unlock()
for ip, client := range rl.clients {
if time.Since(client.lastSeen) > 1*time.Hour {
delete(rl.clients, ip)
}
}
}
type Server struct { type Server struct {
DB *sql.DB DB *sql.DB
Router *http.ServeMux Router *http.ServeMux
Logger *log.Logger Logger *log.Logger
Auth *data.AuthorizationService Auth *data.AuthorizationService
RateLimiter *RateLimiter
}
// getClientIP extracts the client IP address from the request
func getClientIP(r *http.Request) string {
// Check for X-Forwarded-For header first (for clients behind proxy)
ip := r.Header.Get("X-Forwarded-For")
if ip != "" {
// X-Forwarded-For can contain multiple IPs, use the first one
ips := net.ParseIP(ip)
if ips != nil {
return ips.String()
}
}
// Fall back to RemoteAddr
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
} }
func NewServer(db *sql.DB, logger *log.Logger) *Server { func NewServer(db *sql.DB, logger *log.Logger) *Server {
// Create a rate limiter with 10 requests per second and burst of 30
rateLimiter := NewRateLimiter(10, 30)
s := &Server{ s := &Server{
DB: db, DB: db,
Router: http.NewServeMux(), Router: http.NewServeMux(),
Logger: logger, Logger: logger,
Auth: data.NewAuthorizationService(db), Auth: data.NewAuthorizationService(db),
RateLimiter: rateLimiter,
} }
// Start a goroutine to clean up old clients
go func() {
for {
time.Sleep(1 * time.Hour)
rateLimiter.CleanupClients()
}
}()
s.registerRoutes() s.registerRoutes()
return s return s
@ -36,11 +142,76 @@ func (s *Server) registerRoutes() {
s.Router.HandleFunc("GET /v1/database/credentials", s.handleDatabaseCredentials) s.Router.HandleFunc("GET /v1/database/credentials", s.handleDatabaseCredentials)
} }
// sendRateLimitExceeded sends a rate limit exceeded response
func (s *Server) sendRateLimitExceeded(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
response := map[string]string{
"error": "Rate limit exceeded. Please try again later.",
}
if err := json.NewEncoder(w).Encode(response); err != nil {
s.Logger.Printf("Error encoding rate limit response: %v", err)
}
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get client IP
clientIP := getClientIP(r)
// Get rate limiter for this client
limiter := s.RateLimiter.GetLimiter(clientIP)
// Check if rate limit is exceeded
if !limiter.Allow() {
s.Logger.Printf("Rate limit exceeded for IP: %s, URI: %s", clientIP, r.RequestURI)
s.sendRateLimitExceeded(w)
return
}
// Apply stricter rate limiting for authentication endpoints
if r.URL.Path == "/v1/login/password" || r.URL.Path == "/v1/login/token" || r.URL.Path == "/v1/login/totp" {
// Use a separate limiter with lower rate for auth endpoints
authLimiter := rate.NewLimiter(rate.Limit(1), 5) // 1 request per second, burst of 5
if !authLimiter.Allow() {
s.Logger.Printf("Auth rate limit exceeded for IP: %s, URI: %s", clientIP, r.RequestURI)
s.sendRateLimitExceeded(w)
return
}
}
// Proceed with the request
s.Router.ServeHTTP(w, r) s.Router.ServeHTTP(w, r)
} }
// LogSecurityEvent logs a security-related event
func (s *Server) LogSecurityEvent(r *http.Request, eventType string, userID, username string, success bool, details map[string]string) {
event := SecurityEvent{
Timestamp: time.Now().UTC().Format(time.RFC3339),
EventType: eventType,
UserID: userID,
Username: username,
IPAddress: getClientIP(r),
UserAgent: r.UserAgent(),
RequestURI: r.RequestURI,
Success: success,
Details: details,
}
// Convert to JSON for structured logging
eventJSON, err := json.Marshal(event)
if err != nil {
s.Logger.Printf("Error marshaling security event: %v", err)
return
}
// Log the security event
s.Logger.Printf("SECURITY_EVENT: %s", eventJSON)
}
func (s *Server) Start(addr string) error { func (s *Server) Start(addr string) error {
s.Logger.Printf("Starting server on %s", addr) s.Logger.Printf("Starting server on %s", addr)
s.Logger.Printf("Note: This server is designed to run behind a reverse proxy that handles TLS")
return http.ListenAndServe(addr, s) return http.ListenAndServe(addr, s)
} }

View File

@ -1,7 +1,9 @@
package main package main
import ( import (
"crypto/rand"
"database/sql" "database/sql"
"encoding/hex"
"fmt" "fmt"
"log" "log"
"os" "os"
@ -76,7 +78,14 @@ func addToken() {
logger.Fatalf("Failed to check if user exists: %v", err) logger.Fatalf("Failed to check if user exists: %v", err)
} }
token := ulid.Make().String() // Generate 16 bytes of random data
tokenBytes := make([]byte, 16)
if _, err := rand.Read(tokenBytes); err != nil {
logger.Fatalf("Failed to generate random token: %v", err)
}
// Hex encode the random bytes to get a 32-character string
token := hex.EncodeToString(tokenBytes)
expires := time.Now().Add(time.Duration(tokenDuration) * time.Hour).Unix() expires := time.Now().Add(time.Duration(tokenDuration) * time.Hour).Unix()

View File

@ -81,25 +81,11 @@ func (u *User) CheckPassword(login *Login) bool {
return subtle.ConstantTimeCompare(derived, u.Password) == validCompareResult return subtle.ConstantTimeCompare(derived, u.Password) == validCompareResult
} }
// Check is a legacy method that now only checks the password
// It's kept for backward compatibility but is equivalent to CheckPassword
func (u *User) Check(login *Login) bool { func (u *User) Check(login *Login) bool {
// First check username and password // Only check username and password, TOTP verification is now a separate flow
if !u.CheckPassword(login) { return u.CheckPassword(login)
return false
}
// If TOTP is enabled for the user, validate the TOTP code
if u.TOTPSecret != emptyString && login.TOTPCode != emptyString {
// Use the ValidateTOTPCode method to validate the TOTP code
valid, validErr := u.ValidateTOTPCode(login.TOTPCode)
if validErr != nil || !valid {
return false
}
} else if u.TOTPSecret != emptyString && login.TOTPCode == emptyString {
// TOTP is enabled but no code was provided
return false
}
return true
} }
func (u *User) Register(login *Login) error { func (u *User) Register(login *Login) error {

View File

@ -12,7 +12,7 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
**Endpoint**: `POST /v1/login/password` **Endpoint**: `POST /v1/login/password`
**Description**: Authenticates a user using username and password credentials. **Description**: Authenticates a user using username and password credentials. This endpoint does not require TOTP verification, even if TOTP is enabled for the user.
**Request Format**: **Request Format**:
```json ```json
@ -34,13 +34,15 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
```json ```json
{ {
"token": "authentication_token", "token": "authentication_token",
"expires": 1621234567 "expires": 1621234567,
"totp_enabled": true
} }
``` ```
**Response Fields**: **Response Fields**:
- `token`: Authentication token to be used for subsequent requests - `token`: Authentication token to be used for subsequent requests
- `expires`: Unix timestamp when the token expires - `expires`: Unix timestamp when the token expires
- `totp_enabled`: Boolean indicating whether TOTP is enabled for the user
**Error Responses**: **Error Responses**:
- 400 Bad Request: Invalid request format or missing required fields - 400 Bad Request: Invalid request format or missing required fields
@ -86,6 +88,43 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
- 401 Unauthorized: Invalid or expired token - 401 Unauthorized: Invalid or expired token
- 500 Internal Server Error: Server-side error - 500 Internal Server Error: Server-side error
#### TOTP Verification
**Endpoint**: `POST /v1/login/totp`
**Description**: Verifies a TOTP code for a user and issues a token upon successful verification. This endpoint is used as a separate flow from password authentication.
**Request Format**:
```json
{
"version": "v1",
"username": "username",
"totp_code": "123456"
}
```
**Required Fields**:
- `version`: Must be "v1"
- `username`: Username
- `totp_code`: Time-based One-Time Password code
**Response Format** (Success - 200 OK):
```json
{
"token": "authentication_token",
"expires": 1621234567
}
```
**Response Fields**:
- `token`: Authentication token to be used for subsequent requests
- `expires`: Unix timestamp when the token expires
**Error Responses**:
- 400 Bad Request: Invalid request format, missing required fields, or TOTP not enabled for user
- 401 Unauthorized: Invalid TOTP code
- 500 Internal Server Error: Server-side error
### Database Credentials ### Database Credentials
**Endpoint**: `/v1/credentials/database` (Not yet implemented) **Endpoint**: `/v1/credentials/database` (Not yet implemented)
@ -112,14 +151,34 @@ Common HTTP status codes:
## Authentication Flow ## Authentication Flow
### Password Authentication Flow
1. **Initial Authentication**: 1. **Initial Authentication**:
- Client sends username and password to `/v1/login/password` - Client sends username and password to `/v1/login/password`
- Server validates credentials and returns a token - Server validates credentials and returns a token
- The response includes a `totp_enabled` flag indicating whether TOTP is enabled for the user
2. **Subsequent Requests**: 2. **Subsequent Requests**:
- Client uses the token for authentication by sending it to `/v1/login/token` - Client uses the token for authentication by sending it to `/v1/login/token`
- Server validates the token and issues a new token - Server validates the token and issues a new token
3. **Token Expiration**: ### TOTP Authentication Flow
1. **TOTP Verification** (separate from password authentication):
- Client sends username and TOTP code to `/v1/login/totp`
- Server validates the TOTP code and returns a token if valid
### Token Management
1. **Token Expiration**:
- Tokens expire after 24 hours - Tokens expire after 24 hours
- Clients should request a new token before expiration - Clients should request a new token before expiration
### Multi-Factor Authentication
For users with TOTP enabled, a complete multi-factor authentication flow would involve:
1. Authenticate with username and password using `/v1/login/password`
2. Check the `totp_enabled` flag in the response
3. If TOTP is enabled, prompt the user for their TOTP code
4. Verify the TOTP code using `/v1/login/totp` to get a second token
5. Use either token for subsequent requests (both are valid)

View File

@ -91,10 +91,74 @@ MCIAS (Metacircular Identity and Access System) provides identity and authentica
*** Database Credentials *** Database Credentials
*Endpoint*: =/v1/credentials/database= (Not yet implemented) *Endpoint*: =GET /v1/database/credentials=
*Description*: Retrieves database credentials for authorized users. *Description*: Retrieves database credentials for authorized users.
*Request Parameters*:
- =username=: Username of the authenticated user
*Headers*:
- =Authorization=: Bearer token for authentication
*Response Format* (Success - 200 OK):
#+begin_src json
{
"host": "database_host",
"port": 5432,
"name": "database_name",
"user": "database_user",
"password": "database_password"
}
#+end_src
*Error Responses*:
- 400 Bad Request: Invalid request format or missing required parameters
- 401 Unauthorized: Invalid or expired token
- 403 Forbidden: Insufficient permissions to access database credentials
- 500 Internal Server Error: Server-side error
*** TOTP Authentication
*Endpoint*: =POST /v1/login/totp=
*Description*: Authenticates a user using TOTP (Time-based One-Time Password) in addition to username and password.
*Request Format*:
#+begin_src json
{
"version": "v1",
"login": {
"user": "username",
"password": "secret_password",
"totp": "123456"
}
}
#+end_src
*Required Fields*:
- =version=: Must be "v1"
- =login.user=: Username
- =login.password=: User's password
- =login.totp=: 6-digit TOTP code from authenticator app
*Response Format* (Success - 200 OK):
#+begin_src json
{
"token": "authentication_token",
"expires": 1621234567
}
#+end_src
*Response Fields*:
- =token=: Authentication token to be used for subsequent requests
- =expires=: Unix timestamp when the token expires
*Error Responses*:
- 400 Bad Request: Invalid request format or missing required fields
- 401 Unauthorized: Invalid username, password, or TOTP code
- 500 Internal Server Error: Server-side error
** Error Handling ** Error Handling
All error responses follow a standard format: All error responses follow a standard format:
@ -118,11 +182,20 @@ Common HTTP status codes:
1. *Initial Authentication*: 1. *Initial Authentication*:
- Client sends username and password to =/v1/login/password= - Client sends username and password to =/v1/login/password=
- Server validates credentials and returns a token - Server validates credentials and returns a token
- If TOTP is enabled for the user, authentication with password alone will fail
2. *Subsequent Requests*: 2. *TOTP Authentication*:
- For users with TOTP enabled, client sends username, password, and TOTP code to =/v1/login/totp=
- Server validates all credentials and returns a token
3. *Subsequent Requests*:
- Client uses the token for authentication by sending it to =/v1/login/token= - Client uses the token for authentication by sending it to =/v1/login/token=
- Server validates the token and issues a new token - Server validates the token and issues a new token
3. *Token Expiration*: 4. *Token Expiration*:
- Tokens expire after 24 hours - Tokens expire after 24 hours
- Clients should request a new token before expiration - Clients should request a new token before expiration
5. *Database Credential Access*:
- Client sends a GET request to =/v1/database/credentials= with the token in the Authorization header
- Server validates the token and returns the database credentials if the user has sufficient permissions

130
docs/deployment.md Normal file
View File

@ -0,0 +1,130 @@
# MCIAS Deployment Guide
## Overview
This document provides guidance on deploying the Metacircular Identity and Access System (MCIAS) in a production environment. MCIAS is designed to be deployed behind a reverse proxy that handles TLS termination and security headers.
## Prerequisites
- Linux server with systemd
- Nginx or another reverse proxy for TLS termination
- User and group `mcias` created on the system
- Go 1.23 or later for building from source
## Installation
1. Build the MCIAS binary:
```bash
go build -o mcias ./cmd/mcias
```
2. Create the installation directory:
```bash
sudo mkdir -p /opt/mcias
```
3. Copy the binary and service file:
```bash
sudo cp mcias /opt/mcias/
sudo cp mcias.service /etc/systemd/system/
```
4. Set appropriate permissions:
```bash
sudo chown -R mcias:mcias /opt/mcias
sudo chmod 755 /opt/mcias/mcias
```
5. Initialize the database:
```bash
sudo -u mcias /opt/mcias/mcias init --db /opt/mcias/mcias.db
```
6. Enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable mcias
sudo systemctl start mcias
```
## Reverse Proxy Configuration
MCIAS is designed to run behind a reverse proxy that handles TLS termination and security headers. Below is an example Nginx configuration:
```nginx
server {
listen 443 ssl http2;
server_name mcias.example.com;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/mcias.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcias.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Proxy to MCIAS
location / {
proxy_pass http://127.0.0.1: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 mcias.example.com;
return 301 https://$host$request_uri;
}
```
## Security Considerations
1. **TLS Configuration**: The reverse proxy should use modern TLS protocols (TLSv1.2 and TLSv1.3) and strong cipher suites.
2. **Security Headers**: The reverse proxy should add security headers to all responses, as shown in the Nginx configuration example.
3. **Firewall Configuration**: Configure your firewall to only allow connections to the MCIAS server from the reverse proxy.
4. **Regular Updates**: Keep the MCIAS software, operating system, and reverse proxy up to date with security patches.
5. **Monitoring**: Set up monitoring for the MCIAS service and review logs regularly for security events.
## Logging
MCIAS logs to the systemd journal by default. You can view logs using:
```bash
sudo journalctl -u mcias
```
Security events are logged with the prefix `SECURITY_EVENT:` and are in JSON format for easy parsing.
## Backup and Recovery
Regularly back up the MCIAS database file (/opt/mcias/mcias.db) to ensure you can recover in case of data loss.
Example backup script:
```bash
#!/bin/bash
BACKUP_DIR="/var/backups/mcias"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
mkdir -p $BACKUP_DIR
sqlite3 /opt/mcias/mcias.db ".backup $BACKUP_DIR/mcias_$TIMESTAMP.db"
```

View File

@ -31,7 +31,7 @@ go mod download
MCIAS uses SQLite for data storage. To initialize the database: MCIAS uses SQLite for data storage. To initialize the database:
#+begin_src bash #+begin_src bash
go run main.go -init -db ./mcias.db go run cmd/mcias/main.go init --db ./mcias.db
#+end_src #+end_src
This command creates a new SQLite database file and initializes it with the schema defined in =schema.sql=. This command creates a new SQLite database file and initializes it with the schema defined in =schema.sql=.
@ -43,37 +43,76 @@ This command creates a new SQLite database file and initializes it with the sche
To start the MCIAS server with default settings: To start the MCIAS server with default settings:
#+begin_src bash #+begin_src bash
go run main.go -db ./mcias.db go run cmd/mcias/main.go server --db ./mcias.db
#+end_src #+end_src
By default, the server listens on port 8080. By default, the server listens on port 8080.
*** Configuration Options *** Configuration Options
MCIAS supports the following command-line options: MCIAS supports the following command-line options for the server:
- =-db <path>=: Path to the SQLite database file (default: =mcias.db=) - =--db <path>=: Path to the SQLite database file (default: =mcias.db=)
- =-addr <address>=: Address to listen on (default: =:8080=) - =--addr <address>=: Address to listen on (default: =:8080=)
- =-init=: Initialize the database and exit
Example with custom port: Example with custom port:
#+begin_src bash #+begin_src bash
go run main.go -db ./mcias.db -addr :9000 go run cmd/mcias/main.go server --db ./mcias.db --addr :9000
#+end_src
** Managing Users and Authentication
*** Adding a New User
To add a new user to the system:
#+begin_src bash
go run cmd/mcias/main.go user add --username <username> --password <password>
#+end_src
*** Managing TOTP Authentication
To enable TOTP for a user:
#+begin_src bash
go run cmd/mcias/main.go totp enable --username <username>
#+end_src
This will generate a TOTP secret for the user and display it. The user should save this secret in their authenticator app.
To add a TOTP token with QR code generation:
#+begin_src bash
go run cmd/mcias/main.go totp add --username <username> --qr-output <path/to/qrcode.png>
#+end_src
To validate a TOTP code:
#+begin_src bash
go run cmd/mcias/main.go totp validate --username <username> --code <totp_code>
#+end_src #+end_src
** Building from Source ** Building from Source
To build a binary: To build the server binary:
#+begin_src bash #+begin_src bash
cd cmd/mcias
go build -o mcias go build -o mcias
#+end_src #+end_src
Then run the binary: Then run the binary:
#+begin_src bash #+begin_src bash
./mcias -db ./mcias.db ./mcias server --db ./mcias.db
#+end_src
To build the client binary:
#+begin_src bash
cd cmd/mcias-client
go build -o mcias-client
#+end_src #+end_src
** Development ** Development
@ -128,6 +167,32 @@ curl -X POST http://localhost:8080/v1/login/token \
}' }'
#+end_src #+end_src
*** Authentication with TOTP
To authenticate a user with a password and TOTP code:
#+begin_src bash
curl -X POST http://localhost:8080/v1/login/totp \
-H "Content-Type: application/json" \
-d '{
"version": "v1",
"login": {
"user": "username",
"password": "password",
"totp": "123456"
}
}'
#+end_src
*** Retrieving Database Credentials
To retrieve database credentials:
#+begin_src bash
curl -X GET "http://localhost:8080/v1/database/credentials?username=username" \
-H "Authorization: Bearer your_token"
#+end_src
** Troubleshooting ** Troubleshooting
*** Common Issues *** Common Issues
@ -170,4 +235,4 @@ go run main.go -db ./mcias.db > mcias.log 2>&1
3. *User Management*: 3. *User Management*:
- Implement strong password policies - Implement strong password policies
- Regularly rotate tokens - Regularly rotate tokens
- Monitor for suspicious authentication attempts - Monitor for suspicious authentication attempts

View File

@ -11,10 +11,10 @@ The system currently provides:
1. User password authentication 1. User password authentication
2. User token authentication 2. User token authentication
3. Database credential authentication 3. Database credential authentication
4. TOTP (Time-based One-Time Password) authentication
Future planned features include: Future planned features include:
1. TOTP (Time-based One-Time Password) authentication 1. Policy management for fine-grained access control
2. Policy management for fine-grained access control
** System Architecture ** System Architecture
@ -57,7 +57,8 @@ CREATE TABLE users (
created integer, created integer,
user text not null, user text not null,
password blob not null, password blob not null,
salt blob not null salt blob not null,
totp_secret text
); );
#+end_src #+end_src
@ -108,6 +109,24 @@ CREATE TABLE user_roles (
); );
#+end_src #+end_src
*** Permissions Tables
#+begin_src sql
CREATE TABLE permissions (
id TEXT PRIMARY KEY,
resource TEXT NOT NULL,
action TEXT NOT NULL,
description TEXT
);
CREATE TABLE role_permissions (
id TEXT PRIMARY KEY,
rid TEXT NOT NULL,
pid TEXT NOT NULL,
FOREIGN KEY(rid) REFERENCES roles(id),
FOREIGN KEY(pid) REFERENCES permissions(id)
);
#+end_src
** Security Considerations ** Security Considerations
MCIAS implements several security best practices: MCIAS implements several security best practices:
@ -130,4 +149,4 @@ MCIAS implements several security best practices:
4. *Database Security* 4. *Database Security*
- Parameterized queries to prevent SQL injection - Parameterized queries to prevent SQL injection
- Foreign key constraints to maintain data integrity - Foreign key constraints to maintain data integrity

4
go.mod
View File

@ -5,12 +5,15 @@ go 1.23.8
require github.com/gokyle/twofactor v1.0.1 require github.com/gokyle/twofactor v1.0.1
require ( require (
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/golang-migrate/migrate/v4 v4.18.3 // indirect github.com/golang-migrate/migrate/v4 v4.18.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
@ -27,6 +30,7 @@ require (
golang.org/x/crypto v0.38.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
) )

9
go.sum
View File

@ -1,3 +1,5 @@
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -16,6 +18,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
@ -40,6 +46,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@ -55,6 +62,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

25
mcias.service Normal file
View File

@ -0,0 +1,25 @@
[Unit]
Description=Metacircular Identity and Access System
After=network.target
[Service]
Type=simple
User=mcias
Group=mcias
WorkingDirectory=/opt/mcias
ExecStart=/opt/mcias/mcias server --db /opt/mcias/mcias.db
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mcias
# Security settings
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
ReadWritePaths=/opt/mcias
[Install]
WantedBy=multi-user.target