Security#
Viscord holds two credentials that can act on your behalf: a bot token that can read and write as the bot, and a webhook URL that can post into a channel under any name. It also maintains a UUID-to-Discord-ID mapping that is identifying even though it contains no secrets.
This page is about where those live, what the mod does and does not protect, and what you have to do yourself.
Secrets at rest#
| Secret | Location | Format |
|---|---|---|
| Discord bot token | config/viscord/viscord.toml → discord.bot_token |
Plain text |
| Discord webhook URL | config/viscord/viscord.toml → discord.webhook_url |
Plain text |
| Fluxer bot token | config/viscord/viscord.toml → fluxer.bot_token |
Plain text |
| Fluxer webhook URL | config/viscord/viscord.toml → fluxer.webhook_url |
Plain text |
There is no keyring integration, no environment variable substitution, and no
.env loader. The values sit in a TOML file in the server directory. Protect
that file with filesystem permissions — chmod 600 owned by the server user, and
no read access for anyone else — and keep it out of source control.
The two JSON state files contain no secrets but are worth classifying:
| File | Contents | Sensitivity |
|---|---|---|
viscord-links.json |
Minecraft UUID, username, Discord ID, Discord display name, timestamp | Identifying |
viscord-preferences.json |
Per-player boolean toggles | Low |
Both are written with StandardCharsets.UTF_8 explicitly, so a non-ASCII username
cannot be mangled by a platform-default charset.
Log redaction#
Webhook URLs are never logged in full. Both webhook clients route every URL through a redactor that strips the token segment:
url.replaceAll("(/webhooks/\\d+/)[^/?#]+", "$1***")
An error line therefore reads:
Error sending webhook payload to https://discord.com/api/webhooks/123456789012345678/***: ...
The webhook ID survives — it is useful for diagnostics, and it cannot be used to post. The token does not.
Things to know about this guarantee:
- It covers the two webhook clients. It does not cover a token you paste into a chat message, an issue, or a screenshot.
- Redaction happens on the log path. The URL is still passed unredacted to the HTTP client, which is unavoidable.
- Bot tokens are never logged in any form. The connection code logs the bot's
display name on success (
Connected as Name#0000) and the failure reason on error, not the credential.
Account linking hardening#
The six-digit handshake is the only place a player-supplied value influences a persistent binding. Its defences:
| Defence | Implementation |
|---|---|
| Unpredictable codes | SecureRandom, not Random |
| No code collisions | putIfAbsent retry loop |
| Atomic bind | Check-then-insert inside one synchronized block on the account map |
| Strict input | ^\d{6}$; anything else rejected before a lookup |
| Generic errors | One message for every malformed input, so replies cannot be used to enumerate |
| Rate limits | Per-user and global sliding 60-second windows, checked before the lookup |
| Silent throttle | The legacy text trigger does not reply when limited, so an attacker cannot measure the window |
| Expiry | Absolute timestamp per pending code, swept on every generate and verify |
| Single use | The code is removed on success and on every rejection path |
| Encoding | UTF-8 pinned readers and writers |
The rate limiting is the load-bearing control. Against a 1,000,000-code space with 3 attempts per user per minute and a 5-minute code lifetime, sustained guessing from one account is not a practical attack, and the 30-per-minute channel-wide cap bounds a coordinated one.
Account linking should be treated as convenience authentication, not as identity proof. A binding means "whoever typed the code in Discord was the player who generated it" — which is exactly as strong as the secrecy of that code and the security of both accounts at that moment.
Filter integrity#
filters.trusted_bot_ids is the most security-relevant config value in the mod.
What it does: an author ID in this list bypasses ignore_bots and
ignore_webhooks. Their messages are then processed like any other.
What it does not do:
- It does not bypass the self-origin checks. Those run first and unconditionally.
- It does not restrict what the trusted bot can post. A message from a trusted bot can carry any text into your Minecraft chat.
- It does not verify the bot's identity beyond its Discord ID. Anyone who can control that bot, or who can get their own bot ID into your config, gets a channel into your game chat.
So the list is a peer allowlist. Every ID on it is a statement that you trust that bot's operator not to abuse write access to your Minecraft chat. Keep it minimal, and re-read it after any staff or hosting change.
The echo guards themselves are defensive rather than authoritative. The prefix
check is a string comparison on a display name, which is cosmetic: a determined
actor who can post in the channel can name a webhook [MC] and have their message
treated as this server's own. That is not an escalation, because anyone who can
post in the channel can already reach your players through the bridge — but it
does mean the prefix is not a security boundary. The webhook-ID and bot-ID checks
are the boundary, because they compare against values Viscord learned from its own
configuration and its own connection.
Webhook token rotation#
A webhook URL can post into its channel under any name. Treat a leak as a full compromise of the channel's content integrity.
To rotate:
- In the Discord channel, open Integrations → Webhooks, delete the old webhook, and create a new one.
- Put the new URL in
discord.webhook_url. - Run
/viscord reloadas an op-4 operator, or restart.
No restart is strictly required — the webhook URL is read from config on every send. The one thing you lose by not reloading is the self-origin ID check, which compares against the currently configured URL: until reload, the mod compares incoming authors against the old ID and will not recognise its own new webhook as self. Reload promptly after rotating.
Rotating the bot token is the same shape: reset it in the developer portal, update
discord.bot_token, reload.
Least-privilege bot permissions#
The bot needs, per bridged channel:
- View Channel
- Send Messages
- Read Message History
- Embed Links
Optionally Manage Webhooks, and only if something is managing the webhook
through the API. If you create the webhook by hand and paste the URL, the bot does
not need it.
The bot does not need Administrator. Viscord never checks Discord roles or
permissions for anything except /status, which requires the invoking member to
be an administrator — that is a check against the member, not a permission the bot
must hold.
Viscord requests the gateway intents for guilds and messages, deliberately
excluding GUILD_PRESENCES and GUILD_MEMBERS. The bot cannot read your member
list or member presence, even if its token leaks.
Privacy#
Display names. With messages.use_display_name = true (the default), a
Discord author appears in Minecraft under their server nickname, falling back to
their global display name, then their account name. Set it to false to use only
the account name, which is what a privacy-sensitive deployment generally wants —
server nicknames can contain real names.
Attachments and embeds are converted into readable Minecraft text; a Discord attachment becomes a clickable filename, and an embed becomes a bordered block with its title, description, and fields. Nothing is fetched from Discord's CDN beyond the avatar and attachment URLs, which are handed to the client for it to open if the player clicks.
Mentions are not stripped. Viscord does not neutralise @everyone or
@here in either direction. A Minecraft player can type text containing
@everyone and it will arrive in Discord as-is. Discord's own mention handling
decides what that does, and the mitigation is at Discord's permission layer:
deny MENTION_EVERYONE on the webhook's channel, or on the bot's role, so
neither the webhook nor the bot can trigger a mass ping. Do this on any public
server; it is one permission toggle and it closes the whole class of problem.
The link map is identifying. viscord-links.json associates real Minecraft
accounts with real Discord accounts. Back it up deliberately, and treat it as
personal data if your jurisdiction cares about that.
TLS#
All Discord and Fluxer HTTP traffic goes through OkHttp using the JVM's default
SSLSocketFactory and truststore. There is no custom trust manager, no
certificate pinning, and no way to disable verification from config — the secure
path is the only path.
The consequence: if your JVM truststore is wrong, or a proxy has been configured at the JVM level, Viscord inherits that. It will not silently fall back to plaintext.
The Fluxer gateway is a WSS connection to wss://gateway.fluxer.app/?v=1 and is
subject to the same truststore.
What Viscord does not do#
Stated plainly, because assuming otherwise is how gaps happen:
- No Discord-role-to-Minecraft-permission mapping. A Discord administrator is
not a Minecraft operator. The only permission check Viscord performs on Discord
is
/status, and it grants information, not authority. The/homecommand explicitly dispatches at Minecraft permission level 0 for exactly this reason. - No payload signing or HMAC. Outbound webhook payloads are not signed. Integrity rests entirely on Discord's URL-token model — the URL is the credential.
- No config encryption at rest.
viscord.tomlis plain text. Filesystem permissions and disk encryption are your responsibility. - No inbound authentication. Viscord does not run a listener. Discord and Fluxer traffic arrives over outbound-initiated connections, so there is no port to firewall and no shared secret to verify beyond the platform's own auth.
- No audit log of its own. Join, leave, death, and advancement events can be posted to Discord, but Viscord keeps no local history, no message archive, and no record of who said what beyond what Discord itself retains.
- No per-channel authorisation. The configured channel list is a security boundary: anyone who can post in a configured channel can reach Minecraft chat. If a channel is public, so is your game chat.
If you need role mapping, signed payloads, or an audit trail, those have to be layered on top with another mod or an external service.