Security & Permissions
Complete guide to securing your FiveM server and managing player permissions.
Security is paramount for any FiveM server. This guide covers comprehensive security measures, permission systems, and protection against common threats.
Server Security Fundamentals
Basic Server Hardening
Network Security
# Basic firewall configuration for FiveM server
# Allow only necessary ports
# Ubuntu/Debian
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 30120/tcp # FiveM default port
sudo ufw allow 30120/udp # FiveM default port
sudo ufw allow 22/tcp # SSH (change default port)
sudo ufw allow 3306/tcp from 127.0.0.1 # MySQL local only
sudo ufw enable
# CentOS/RHEL
firewall-cmd --permanent --add-port=30120/tcp
firewall-cmd --permanent --add-port=30120/udp
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
System User Configuration
# Create dedicated FiveM user with limited privileges
sudo useradd -m -s /bin/bash fivem
sudo usermod -aG sudo fivem # Only if admin access needed
# Set up proper directory permissions
sudo mkdir -p /opt/fivem
sudo chown fivem:fivem /opt/fivem
sudo chmod 755 /opt/fivem
# Secure sensitive files
chmod 600 /opt/fivem/server.cfg
chmod 600 /opt/fivem/database.cfg
server.cfg Security Configuration
# Basic security settings
sv_hostname "My Secure FiveM Server"
sv_maxclients 64
# Network security
endpoint_add_tcp "0.0.0.0:30120"
endpoint_add_udp "0.0.0.0:30120"
# Disable unnecessary features
sv_scriptHookAllowed 0
sv_enableChatPrivateMessage 0
# Rate limiting
sv_playerConnectionsLogHistory 100
sv_playerConnectionsLogHistorySize 1000
# Security headers
set sv_enforceGameBuild 2699
set sv_licenseKeyToken "your_license_key_here"
# Database security (if using MySQL)
set mysql_connection_string "mysql://username:password@localhost/database?charset=utf8mb4"
# Admin security
add_principal identifier.steam:110000100000000 group.admin
add_ace group.admin command.restart allow
add_ace group.admin command.stop allow
# Anti-cheat configuration
sv_authMaxVariance 1
sv_authMinTrust 5
# Logging for security monitoring
set sv_logFile "server.log"
set sv_logLevel "info"
# Resource security
start sessionmanager
start chat
start spawnmanager
start hardcap
start rconlog
# Custom security resources
start anticheat
start security-monitor
Advanced Security Measures
Custom Anti-Cheat Implementation
-- resources/anticheat/server/main.lua
local SecurityConfig = {
maxSpeed = 300.0, -- km/h
maxHealthGain = 5.0, -- per second
maxArmorGain = 5.0, -- per second
weaponWhitelist = {
GetHashKey("WEAPON_PISTOL"),
GetHashKey("WEAPON_KNIFE"),
-- Add allowed weapons
},
bannedWords = {
"cheat", "hack", "mod menu", "trainer"
}
}
-- Player monitoring
local playerData = {}
-- Initialize player data
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local source = source
playerData[source] = {
lastPosition = vector3(0, 0, 0),
lastHealth = 200,
lastArmor = 0,
violations = 0,
joinTime = GetGameTimer()
}
end)
-- Speed check
Citizen.CreateThread(function()
while true do
Citizen.Wait(1000)
for playerId, data in pairs(playerData) do
if GetPlayerPing(playerId) > 0 then
local playerPed = GetPlayerPed(playerId)
local coords = GetEntityCoords(playerPed)
if data.lastPosition then
local distance = #(coords - data.lastPosition)
local speed = (distance * 3.6) -- Convert to km/h
if speed > SecurityConfig.maxSpeed then
data.violations = data.violations + 1
TriggerEvent('anticheat:violation', playerId, 'speed', speed)
end
end
data.lastPosition = coords
end
end
end
end)
-- Health/Armor monitoring
Citizen.CreateThread(function()
while true do
Citizen.Wait(1000)
for playerId, data in pairs(playerData) do
if GetPlayerPing(playerId) > 0 then
local playerPed = GetPlayerPed(playerId)
local health = GetEntityHealth(playerPed)
local armor = GetPedArmour(playerPed)
-- Check for unusual health/armor gains
if health > data.lastHealth + SecurityConfig.maxHealthGain then
TriggerEvent('anticheat:violation', playerId, 'health', health - data.lastHealth)
end
if armor > data.lastArmor + SecurityConfig.maxArmorGain then
TriggerEvent('anticheat:violation', playerId, 'armor', armor - data.lastArmor)
end
data.lastHealth = health
data.lastArmor = armor
end
end
end
end)
-- Weapon monitoring
AddEventHandler('weaponDamageEvent', function(sender, data)
local weaponHash = data.weaponType
local playerId = sender
-- Check if weapon is allowed
local isAllowed = false
for _, allowedWeapon in ipairs(SecurityConfig.weaponWhitelist) do
if weaponHash == allowedWeapon then
isAllowed = true
break
end
end
if not isAllowed then
TriggerEvent('anticheat:violation', playerId, 'weapon', weaponHash)
end
end)
-- Chat monitoring
AddEventHandler('chatMessage', function(source, name, message)
local playerId = source
local lowerMessage = string.lower(message)
for _, bannedWord in ipairs(SecurityConfig.bannedWords) do
if string.find(lowerMessage, string.lower(bannedWord)) then
TriggerEvent('anticheat:violation', playerId, 'chat', bannedWord)
CancelEvent()
return
end
end
end)
-- Violation handler
AddEventHandler('anticheat:violation', function(playerId, violationType, value)
local playerName = GetPlayerName(playerId)
local identifier = GetPlayerIdentifier(playerId, 0)
-- Log violation
print(string.format('[ANTICHEAT] %s (%s) - %s violation: %s',
playerName, identifier, violationType, tostring(value)))
-- Increment violations
if playerData[playerId] then
playerData[playerId].violations = playerData[playerId].violations + 1
-- Take action based on violation count
if playerData[playerId].violations >= 3 then
-- Ban player
DropPlayer(playerId, 'Kicked for cheating violations')
TriggerEvent('anticheat:ban', identifier, playerName, violationType)
elseif playerData[playerId].violations >= 2 then
-- Warn player
TriggerClientEvent('chat:addMessage', playerId, {
color = {255, 0, 0},
multiline = true,
args = {"[ANTICHEAT]", "Warning: Suspicious activity detected!"}
})
end
end
end)
-- Cleanup on disconnect
AddEventHandler('playerDropped', function(reason)
local playerId = source
playerData[playerId] = nil
end)
DDoS Protection
-- resources/ddos-protection/server/main.lua
local ConnectionLimiter = {
maxConnectionsPerIP = 3,
connectionTimeout = 30000, -- 30 seconds
connections = {},
bannedIPs = {}
}
-- Rate limiting for connections
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
local source = source
local ip = GetPlayerEndpoint(source):match("([^:]+)")
if not ip then
setKickReason("Unable to verify connection")
CancelEvent()
return
end
-- Check if IP is banned
if ConnectionLimiter.bannedIPs[ip] then
setKickReason("Your IP address has been temporarily banned")
CancelEvent()
return
end
-- Initialize IP tracking
if not ConnectionLimiter.connections[ip] then
ConnectionLimiter.connections[ip] = {
count = 0,
lastConnection = 0
}
end
local currentTime = GetGameTimer()
local ipData = ConnectionLimiter.connections[ip]
-- Reset count if timeout exceeded
if currentTime - ipData.lastConnection > ConnectionLimiter.connectionTimeout then
ipData.count = 0
end
-- Check connection limit
if ipData.count >= ConnectionLimiter.maxConnectionsPerIP then
setKickReason("Too many connections from your IP address")
-- Temporarily ban IP
ConnectionLimiter.bannedIPs[ip] = currentTime
CancelEvent()
return
end
-- Update connection data
ipData.count = ipData.count + 1
ipData.lastConnection = currentTime
end)
-- Cleanup banned IPs (remove bans after 1 hour)
Citizen.CreateThread(function()
while true do
Citizen.Wait(60000) -- Check every minute
local currentTime = GetGameTimer()
for ip, banTime in pairs(ConnectionLimiter.bannedIPs) do
if currentTime - banTime > 3600000 then -- 1 hour
ConnectionLimiter.bannedIPs[ip] = nil
end
end
end
end)
Permission Systems
ACE Permission Framework
Basic ACE Setup
# server.cfg - ACE permissions setup
# Define groups
add_principal group.admin group.user
add_principal group.moderator group.user
add_principal group.vip group.user
# Admin permissions
add_ace group.admin command allow
add_ace group.admin resource allow
add_ace group.admin builtin.everyone allow
# Moderator permissions
add_ace group.moderator command.kick allow
add_ace group.moderator command.ban allow
add_ace group.moderator command.tempban allow
add_ace group.moderator command.unban allow
# VIP permissions
add_ace group.vip command.car allow
add_ace group.vip command.tp allow
# Specific user assignments
add_principal identifier.steam:110000100000000 group.admin
add_principal identifier.discord:123456789012345678 group.moderator
# Resource-specific permissions
add_ace group.admin esx.setjob allow
add_ace group.admin esx.setmoney allow
add_ace group.moderator esx.kick allow
Advanced Permission Management
-- resources/permission-manager/server/main.lua
local PermissionManager = {
permissions = {},
userGroups = {},
groupPermissions = {}
}
-- Load permissions from database
function LoadPermissions()
local query = [[
SELECT
users.identifier,
groups.name as group_name,
permissions.permission
FROM user_groups users
LEFT JOIN groups ON users.group_id = groups.id
LEFT JOIN group_permissions gp ON groups.id = gp.group_id
LEFT JOIN permissions ON gp.permission_id = permissions.id
]]
exports.oxmysql:execute(query, {}, function(results)
for _, row in ipairs(results) do
if not PermissionManager.userGroups[row.identifier] then
PermissionManager.userGroups[row.identifier] = {}
end
table.insert(PermissionManager.userGroups[row.identifier], row.group_name)
if not PermissionManager.groupPermissions[row.group_name] then
PermissionManager.groupPermissions[row.group_name] = {}
end
if row.permission then
table.insert(PermissionManager.groupPermissions[row.group_name], row.permission)
end
end
print("[PERMISSIONS] Loaded permissions for " .. #results .. " users")
end)
end
-- Check if user has permission
function HasPermission(identifier, permission)
local userGroups = PermissionManager.userGroups[identifier] or {}
for _, group in ipairs(userGroups) do
local groupPerms = PermissionManager.groupPermissions[group] or {}
for _, perm in ipairs(groupPerms) do
if perm == permission or perm == "*" then
return true
end
end
end
return false
end
-- Add user to group
function AddUserToGroup(identifier, groupName)
local query = [[
INSERT INTO user_groups (identifier, group_id)
SELECT ?, id FROM groups WHERE name = ?
ON DUPLICATE KEY UPDATE group_id = VALUES(group_id)
]]
exports.oxmysql:execute(query, {identifier, groupName}, function(result)
if result.affectedRows > 0 then
-- Update in-memory data
if not PermissionManager.userGroups[identifier] then
PermissionManager.userGroups[identifier] = {}
end
if not table.contains(PermissionManager.userGroups[identifier], groupName) then
table.insert(PermissionManager.userGroups[identifier], groupName)
end
return true
end
return false
end)
end
-- Remove user from group
function RemoveUserFromGroup(identifier, groupName)
local query = [[
DELETE ug FROM user_groups ug
INNER JOIN groups g ON ug.group_id = g.id
WHERE ug.identifier = ? AND g.name = ?
]]
exports.oxmysql:execute(query, {identifier, groupName}, function(result)
if result.affectedRows > 0 then
-- Update in-memory data
local userGroups = PermissionManager.userGroups[identifier] or {}
for i, group in ipairs(userGroups) do
if group == groupName then
table.remove(userGroups, i)
break
end
end
return true
end
return false
end)
end
-- Export functions
exports('hasPermission', HasPermission)
exports('addUserToGroup', AddUserToGroup)
exports('removeUserFromGroup', RemoveUserFromGroup)
-- Commands
RegisterCommand('addperm', function(source, args, rawCommand)
local identifier = GetPlayerIdentifier(source, 0)
if not HasPermission(identifier, 'admin.permissions') then
TriggerClientEvent('chat:addMessage', source, {
color = {255, 0, 0},
args = {"[ERROR]", "You don't have permission to use this command"}
})
return
end
if #args < 2 then
TriggerClientEvent('chat:addMessage', source, {
color = {255, 255, 0},
args = {"[USAGE]", "/addperm <player_id> <group>"}
})
return
end
local targetId = tonumber(args[1])
local groupName = args[2]
local targetIdentifier = GetPlayerIdentifier(targetId, 0)
if targetIdentifier then
AddUserToGroup(targetIdentifier, groupName)
TriggerClientEvent('chat:addMessage', source, {
color = {0, 255, 0},
args = {"[SUCCESS]", "Added " .. GetPlayerName(targetId) .. " to group " .. groupName}
})
else
TriggerClientEvent('chat:addMessage', source, {
color = {255, 0, 0},
args = {"[ERROR]", "Player not found"}
})
end
end)
-- Initialize
AddEventHandler('onResourceStart', function(resourceName)
if GetCurrentResourceName() == resourceName then
LoadPermissions()
end
end)
Database Schema for Permissions
-- Permission system database schema
CREATE TABLE IF NOT EXISTS `groups` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) UNIQUE NOT NULL,
`display_name` VARCHAR(100) NOT NULL,
`color` VARCHAR(7) DEFAULT '#FFFFFF',
`priority` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `permissions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`permission` VARCHAR(100) UNIQUE NOT NULL,
`description` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS `group_permissions` (
`group_id` INT,
`permission_id` INT,
PRIMARY KEY (`group_id`, `permission_id`),
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `user_groups` (
`identifier` VARCHAR(100),
`group_id` INT,
`assigned_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`assigned_by` VARCHAR(100),
PRIMARY KEY (`identifier`, `group_id`),
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS `user_permissions` (
`identifier` VARCHAR(100),
`permission_id` INT,
`granted` BOOLEAN DEFAULT TRUE,
`assigned_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`assigned_by` VARCHAR(100),
PRIMARY KEY (`identifier`, `permission_id`),
FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON DELETE CASCADE
);
-- Insert default data
INSERT IGNORE INTO `groups` (`name`, `display_name`, `color`, `priority`) VALUES
('superadmin', 'Super Administrator', '#FF0000', 100),
('admin', 'Administrator', '#FF6600', 90),
('moderator', 'Moderator', '#FFFF00', 80),
('vip', 'VIP Member', '#00FFFF', 20),
('user', 'User', '#FFFFFF', 10);
INSERT IGNORE INTO `permissions` (`permission`, `description`) VALUES
('*', 'All permissions'),
('admin.*', 'All admin permissions'),
('admin.permissions', 'Manage user permissions'),
('admin.server', 'Server management commands'),
('moderator.kick', 'Kick players'),
('moderator.ban', 'Ban players'),
('moderator.unban', 'Unban players'),
('user.basic', 'Basic user permissions');
-- Default group permissions
INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`)
SELECT g.id, p.id FROM `groups` g, `permissions` p
WHERE g.name = 'superadmin' AND p.permission = '*';
INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`)
SELECT g.id, p.id FROM `groups` g, `permissions` p
WHERE g.name = 'admin' AND p.permission = 'admin.*';
INSERT IGNORE INTO `group_permissions` (`group_id`, `permission_id`)
SELECT g.id, p.id FROM `groups` g, `permissions` p
WHERE g.name = 'moderator' AND p.permission IN ('moderator.kick', 'moderator.ban', 'moderator.unban');
Security Monitoring & Logging
Comprehensive Security Logger
-- resources/security-logger/server/main.lua
local SecurityLogger = {
logFile = "logs/security.log",
maxLogSize = 50 * 1024 * 1024, -- 50MB
alertThresholds = {
connectionsPerMinute = 10,
violationsPerHour = 5,
failedLoginsPerHour = 3
},
stats = {
connections = {},
violations = {},
failedLogins = {}
}
}
-- Initialize logging
function InitializeLogger()
-- Create logs directory if it doesn't exist
os.execute("mkdir -p logs")
-- Rotate log file if too large
local file = io.open(SecurityLogger.logFile, "r")
if file then
local size = file:seek("end")
file:close()
if size > SecurityLogger.maxLogSize then
os.rename(SecurityLogger.logFile, SecurityLogger.logFile .. ".old")
end
end
end
-- Log security event
function LogSecurityEvent(eventType, playerId, data)
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
local playerName = playerId and GetPlayerName(playerId) or "SYSTEM"
local identifier = playerId and GetPlayerIdentifier(playerId, 0) or "N/A"
local ip = playerId and GetPlayerEndpoint(playerId):match("([^:]+)") or "N/A"
local logEntry = string.format(
"[%s] [%s] Player: %s (%s) IP: %s Data: %s\n",
timestamp, eventType, playerName, identifier, ip, json.encode(data)
)
-- Write to file
local file = io.open(SecurityLogger.logFile, "a")
if file then
file:write(logEntry)
file:close()
end
-- Print to console
print("[SECURITY] " .. logEntry:gsub("\n", ""))
-- Update statistics
UpdateSecurityStats(eventType, ip, identifier)
-- Check for alerts
CheckSecurityAlerts(eventType, ip, identifier)
end
-- Update security statistics
function UpdateSecurityStats(eventType, ip, identifier)
local currentTime = os.time()
local currentHour = math.floor(currentTime / 3600)
local currentMinute = math.floor(currentTime / 60)
if eventType == "CONNECTION" then
if not SecurityLogger.stats.connections[currentMinute] then
SecurityLogger.stats.connections[currentMinute] = {}
end
SecurityLogger.stats.connections[currentMinute][ip] = (SecurityLogger.stats.connections[currentMinute][ip] or 0) + 1
elseif eventType == "VIOLATION" then
if not SecurityLogger.stats.violations[currentHour] then
SecurityLogger.stats.violations[currentHour] = {}
end
SecurityLogger.stats.violations[currentHour][identifier] = (SecurityLogger.stats.violations[currentHour][identifier] or 0) + 1
elseif eventType == "FAILED_LOGIN" then
if not SecurityLogger.stats.failedLogins[currentHour] then
SecurityLogger.stats.failedLogins[currentHour] = {}
end
SecurityLogger.stats.failedLogins[currentHour][ip] = (SecurityLogger.stats.failedLogins[currentHour][ip] or 0) + 1
end
end
-- Check for security alerts
function CheckSecurityAlerts(eventType, ip, identifier)
local currentTime = os.time()
local currentHour = math.floor(currentTime / 3600)
local currentMinute = math.floor(currentTime / 60)
if eventType == "CONNECTION" then
local connectionsThisMinute = SecurityLogger.stats.connections[currentMinute] and
SecurityLogger.stats.connections[currentMinute][ip] or 0
if connectionsThisMinute >= SecurityLogger.alertThresholds.connectionsPerMinute then
TriggerEvent('security:alert', 'HIGH_CONNECTION_RATE', {
ip = ip,
connections = connectionsThisMinute,
timeframe = "1 minute"
})
end
elseif eventType == "VIOLATION" then
local violationsThisHour = SecurityLogger.stats.violations[currentHour] and
SecurityLogger.stats.violations[currentHour][identifier] or 0
if violationsThisHour >= SecurityLogger.alertThresholds.violationsPerHour then
TriggerEvent('security:alert', 'HIGH_VIOLATION_RATE', {
identifier = identifier,
violations = violationsThisHour,
timeframe = "1 hour"
})
end
end
end
-- Event handlers
AddEventHandler('playerConnecting', function(name, setKickReason, deferrals)
LogSecurityEvent('CONNECTION', source, {
playerName = name,
endpoint = GetPlayerEndpoint(source)
})
end)
AddEventHandler('anticheat:violation', function(playerId, violationType, value)
LogSecurityEvent('VIOLATION', playerId, {
type = violationType,
value = value
})
end)
AddEventHandler('security:alert', function(alertType, data)
LogSecurityEvent('ALERT', nil, {
type = alertType,
data = data
})
-- Send Discord notification
TriggerEvent('discord:sendAlert', alertType, data)
end)
-- Command to view security stats
RegisterCommand('secstats', function(source, args, rawCommand)
local identifier = GetPlayerIdentifier(source, 0)
if not exports['permission-manager']:hasPermission(identifier, 'admin.security') then
return
end
local currentTime = os.time()
local currentHour = math.floor(currentTime / 3600)
local currentMinute = math.floor(currentTime / 60)
local stats = {
connectionsThisMinute = 0,
violationsThisHour = 0,
failedLoginsThisHour = 0
}
-- Count current stats
if SecurityLogger.stats.connections[currentMinute] then
for ip, count in pairs(SecurityLogger.stats.connections[currentMinute]) do
stats.connectionsThisMinute = stats.connectionsThisMinute + count
end
end
TriggerClientEvent('chat:addMessage', source, {
color = {0, 255, 255},
multiline = true,
args = {"[SECURITY STATS]", string.format(
"Connections this minute: %d\nViolations this hour: %d\nFailed logins this hour: %d",
stats.connectionsThisMinute,
stats.violationsThisHour,
stats.failedLoginsThisHour
)}
})
end)
-- Initialize on resource start
AddEventHandler('onResourceStart', function(resourceName)
if GetCurrentResourceName() == resourceName then
InitializeLogger()
print("[SECURITY LOGGER] Initialized")
end
end)
-- Export functions
exports('logSecurityEvent', LogSecurityEvent)
Discord Security Alerts
-- resources/security-logger/server/discord.lua
local DiscordConfig = {
webhook = GetConvar("discord_security_webhook", ""),
botName = "FiveM Security Bot",
embedColor = {
LOW = 65280, -- Green
MEDIUM = 16776960, -- Yellow
HIGH = 16711680, -- Red
CRITICAL = 8388736 -- Dark Red
}
}
function SendDiscordAlert(alertType, data)
if DiscordConfig.webhook == "" then
return
end
local severity = GetAlertSeverity(alertType)
local embed = CreateAlertEmbed(alertType, data, severity)
PerformHttpRequest(DiscordConfig.webhook, function(err, text, headers) end, 'POST', json.encode({
username = DiscordConfig.botName,
embeds = {embed}
}), {['Content-Type'] = 'application/json'})
end
function GetAlertSeverity(alertType)
local severityMap = {
HIGH_CONNECTION_RATE = "HIGH",
HIGH_VIOLATION_RATE = "HIGH",
DDOS_DETECTED = "CRITICAL",
UNAUTHORIZED_ACCESS = "CRITICAL",
CHEAT_DETECTED = "MEDIUM",
SUSPICIOUS_ACTIVITY = "MEDIUM"
}
return severityMap[alertType] or "LOW"
end
function CreateAlertEmbed(alertType, data, severity)
local timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
local embed = {
title = "🚨 Security Alert: " .. alertType,
color = DiscordConfig.embedColor[severity],
timestamp = timestamp,
fields = {}
}
-- Add severity field
table.insert(embed.fields, {
name = "Severity",
value = severity,
inline = true
})
-- Add alert-specific fields
if alertType == "HIGH_CONNECTION_RATE" then
table.insert(embed.fields, {
name = "IP Address",
value = data.ip or "Unknown",
inline = true
})
table.insert(embed.fields, {
name = "Connections",
value = tostring(data.connections) .. " in " .. data.timeframe,
inline = true
})
elseif alertType == "HIGH_VIOLATION_RATE" then
table.insert(embed.fields, {
name = "Player",
value = data.identifier or "Unknown",
inline = true
})
table.insert(embed.fields, {
name = "Violations",
value = tostring(data.violations) .. " in " .. data.timeframe,
inline = true
})
end
-- Add server info
table.insert(embed.fields, {
name = "Server",
value = GetConvar("sv_hostname", "FiveM Server"),
inline = false
})
return embed
end
-- Register event handler
AddEventHandler('discord:sendAlert', SendDiscordAlert)
This comprehensive security and permissions guide provides robust protection for your FiveM server, covering everything from basic hardening to advanced threat detection and response systems.