Network Security¶
Vanguard applies one server-authoritative guard pipeline to every inbound remote function, client-fired signal, remote property read, and replicator read.
This guide explains the threat model, rule configuration, callback contracts, rejection behavior, statistics, and secure patterns.
Threat Model¶
Assume a client can:
- call any replicated RemoteFunction or RemoteEvent;
- call it at arbitrary frequency;
- send values of any Roblox-serializable type;
- send malformed, oversized, stale, or contradictory data;
- replay previously-valid requests;
- inspect all client code and replicated values.
A client cannot choose the Player argument Roblox supplies to OnServerInvoke or OnServerEvent. Vanguard uses that server-supplied Player as the identity key for rate limiting and callbacks.
Do not put a secret token in client code and treat it as authentication. Exploiters can read it. Authenticate against server-owned session, profile, ban, role, or capability state.
Service Rules¶
Rules live beside a service's Client definition and use the same remote keys:
local Validator = require(Vanguard.Util.Validator)
local TradeService = Vanguard.CreateService({
Name = "TradeService",
Client = {
Offer = function(self, player, targetUserId, itemIds)
return self.Server:Offer(player, targetUserId, itemIds)
end,
OfferChanged = Vanguard.CreateSignal(),
},
Network = {
Default = {
RateLimit = { Limit = 20, Window = 1 },
},
Offer = {
Validate = Validator.tuple(
Validator.integer({ Min = 1 }),
Validator.array(Validator.string({ MinLength = 1, MaxLength = 64 }), {
MinLength = 1,
MaxLength = 20,
})
),
Authenticate = function(player, context)
return ProfileService:IsLoaded(player), "Profile is not loaded"
end,
Verify = function(player, context, targetUserId, itemIds)
return context.Service:CanOffer(player, targetUserId, itemIds), "Trade is not permitted"
end,
RateLimit = { Limit = 3, Window = 2 },
},
},
})
Rule fields:
| Field | Callback or value | Purpose |
|---|---|---|
RateLimit |
Options, limiter, false, or nil |
Per-player rolling-window request cap |
Validate |
(...payload) -> boolean, reason? |
Payload type and shape checks |
Authenticate |
(player, context) -> boolean, reason? |
Server-owned identity/session checks |
Verify |
(player, context, ...payload) -> boolean, reason? |
Action authorization and contextual checks |
Unknown rule fields fail startup. Rules whose names do not match a Client remote also fail startup.
Defaults and Overrides¶
Network.Default applies to every remote in one service. A named rule overrides fields shallowly.
Network = {
Default = {
Authenticate = requireAuthenticatedProfile,
RateLimit = { Limit = 20, Window = 1 },
},
PublicStatus = {
Authenticate = false,
RateLimit = { Limit = 5, Window = 1 },
},
}
Set a field to false to disable an inherited service/global-default rule field for one remote. Global top-level Network.Authenticate and Network.Verify callbacks are separate and cannot be disabled by service rules.
Global Policy¶
Configure server-wide policy through start options:
Vanguard.Start({
Network = {
Default = {
RateLimit = { Limit = 60, Window = 1 },
},
Authenticate = function(player, context)
return not BanService:IsBanned(player), "Access denied"
end,
Verify = function(player, context, ...)
return player.Parent ~= nil, "Player is leaving"
end,
OnRejected = function(context, rejection)
SecurityMetrics:Record(context, rejection)
end,
LogRejected = true,
},
})
Global default rules merge first, then service defaults, then named remote rules. Global Authenticate and Verify callbacks run independently after validation.
Enforcement Order¶
Vanguard evaluates requests in this order:
- RateLimit: consumes one unit for the server-supplied Player.
- Validate: checks only the client payload.
- Global Authenticate: checks global identity/session policy.
- Remote Authenticate: checks merged service/remote authentication.
- Global Verify: checks global contextual policy.
- Remote Verify: authorizes the specific action.
Rate limiting happens before validation so malformed requests still consume capacity.
Every callback must explicitly return true to pass. Returning nil, false, or throwing rejects the request.
Context¶
Authentication, verification, and rejection callbacks receive a context table:
type NetworkContext = {
Player: Player,
Service: any,
ServiceName: string,
RemoteName: string,
RemoteType: "Function" | "Signal" | "Property" | "Replicator",
Arguments: { any } & { n: number },
}
Arguments is packed and includes n, preserving trailing nil values.
Use context.ServiceName and context.RemoteName for metrics. Use the callback's explicit payload parameters for readable verification logic.
Rejections¶
type NetworkRejection = {
Code: string,
Message: string,
Stage: string,
RetryAfter: number?,
Detail: any?,
}
Built-in codes:
| Code | Meaning |
|---|---|
RATE_LIMITED |
The Player exceeded the configured rolling-window limit |
INVALID_PAYLOAD |
Validation returned false |
UNAUTHENTICATED |
An authentication callback returned false |
UNVERIFIED |
A verification callback returned false |
GUARD_ERROR |
A guard callback or limiter threw |
Client-visible function, property, and replicator read rejections retain these names and include a direct catalog link. See the network error mapping.
RetryAfter is set for rate-limit rejections. Detail contains internal callback errors and is used only in server logging/callbacks; the formatted client error remains generic for guard failures.
Transport Behavior¶
Remote Functions¶
Rejected calls throw a formatted server error:
In default client promise mode, this rejects the promise.
Remote Signals¶
Rejected client events are dropped before any server listener runs. The client receives no response because RemoteEvents are one-way.
Remote Properties¶
Property Get requests are authenticated and verified. Rejected reads throw like remote functions. Property update delivery from server to client is not an inbound client request and does not run guards.
Replicators¶
Replicator Get requests are authenticated and verified with
RemoteType = "Replicator". Rejected reads throw like remote functions.
Replicator update delivery from server to client is not an inbound client
request and does not run guards.
Rejection Logging¶
Logging is enabled unless LogRejected = false.
To avoid output floods, identical rejection logs are throttled for five seconds per Player, service, remote, and rejection code. Statistics and OnRejected still process every rejection.
Disable logs when a custom security pipeline owns reporting:
Network = {
LogRejected = false,
OnRejected = function(context, rejection)
-- Custom reporting.
end,
}
If OnRejected throws, Vanguard logs that callback failure.
Statistics¶
Server only:
local stats = Vanguard.GetNetworkStats()
print(stats.Accepted)
print(stats.Rejected)
print(stats.ByCode.RATE_LIMITED)
for remoteName, remoteStats in stats.ByRemote do
print(remoteName, remoteStats.Accepted, remoteStats.Rejected)
end
Stats count all inbound requests, including requests to remotes without explicit rules.
Reset counters:
Returned stats are cloned and can be safely modified by the caller.
Rate Limit Configuration¶
Options create a dedicated limiter for each remote:
WeakKeys defaults to true for framework-created network limiters so departed Player keys can be collected.
An existing limiter may be shared intentionally:
local sharedLimiter = Vanguard.CreateRateLimiter({
Limit = 20,
Window = 1,
WeakKeys = true,
})
Network = {
FirstRemote = { RateLimit = sharedLimiter },
SecondRemote = { RateLimit = sharedLimiter },
}
Both remotes then consume the same per-Player budget.
Authentication Pattern¶
Authentication checks whether a Player currently has a valid server-owned session:
Authenticate = function(player)
local profile = ProfileService:GetProfile(player)
return profile ~= nil and profile:IsActive(), "Profile unavailable"
end
Good authentication inputs:
- loaded profile/session state;
- server-maintained login or tutorial completion state;
- server-owned role or access capability;
- ban and moderation state.
Bad authentication inputs:
- a client-provided boolean;
- a token embedded in a LocalScript;
- a replicated StringValue treated as secret;
- a UserId sent in the payload instead of
player.UserId.
Verification Pattern¶
Verification checks the requested action against authoritative state:
Verify = function(player, context, itemId, targetSlot)
local inventory = InventoryService:GetInventory(player)
if not inventory:Owns(itemId) then
return false, "Item is not owned"
end
if not inventory:IsValidSlot(targetSlot) then
return false, "Invalid slot"
end
return true
end
Re-check the same critical conditions in mutation code when concurrency can change state between verification and mutation. Guards improve boundaries; they do not provide transactions.
Validation Pattern¶
Validate = Validator.tuple(
Validator.string({ MinLength = 1, MaxLength = 64 }),
Validator.shape({
Slot = Validator.integer({ Min = 1, Max = 10 }),
Equipped = Validator.boolean(),
})
)
Validation should reject impossible types, unexpected keys, oversized arrays/strings, NaN, infinity, and out-of-range numbers before application logic sees them.
See the complete Validator guide.
Protocol Verification¶
The server stamps _VanguardRemotes with:
VanguardProtocol;VanguardVersion.
The client rejects an incompatible protocol before building service proxies and warns when package versions differ.
Protocol verification prevents accidental client/server framework incompatibility. It is not player authentication and does not prove that a client is unmodified.
Current protocol: 2.
The protocol also specifies remote hierarchy, publication timing, Player identity, function and signal call shapes, property and replicator transport, rejection behavior, and compatibility gating. See the complete Network Protocols reference.
Security Checklist¶
- Validate every mutable or sensitive request payload.
- Cap string and collection sizes.
- Reject NaN and infinity for numeric gameplay values.
- Rate-limit expensive and state-changing operations.
- Use
playersupplied by Roblox, never a payload UserId as identity. - Authenticate against server-owned state.
- Verify ownership, distance, cooldown, currency, and permissions on the server.
- Treat signal payloads as untrusted exactly like function payloads.
- Monitor rejection statistics for abuse and configuration mistakes.
- Keep mutation logic authoritative even after a guard passes.