Junie: security cleanups.
This commit is contained in:
parent
95d96732d2
commit
23c7a65799
43
README.org
43
README.org
|
@ -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
|
||||||
|
|
240
api/auth.go
240
api/auth.go
|
@ -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
|
||||||
|
|
187
api/server.go
187
api/server.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
22
data/user.go
22
data/user.go
|
@ -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 {
|
||||||
|
|
67
docs/api.md
67
docs/api.md
|
@ -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)
|
||||||
|
|
81
docs/api.org
81
docs/api.org
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
|
@ -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
4
go.mod
|
@ -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
9
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue