README
¶
A community Terraform provider for Remnawave — a modern proxy management panel built on top of Xray-core. Manage VPN users, nodes, hosts, squads, billing, subscription pages, and more as infrastructure-as-code.
Table of Contents
- Why Terraform + Remnawave?
- Compatibility
- Quick Start
- Authentication
- Provider Configuration
- Resources
- Data Sources
- Examples
- Importing Existing Resources
- Versioning & Upgrades
- Release Integrity
- Development
- Contributing
- License
Why Terraform + Remnawave?
| Without Terraform | With this provider |
|---|---|
| Create users one-by-one in the web UI | Define hundreds of users in HCL, terraform apply |
| Manual config drift between nodes | Version-controlled, reproducible node configs |
| No audit trail for changes | Git history + Terraform plan/apply logs |
| Onboarding = manual clicks | Onboarding = terraform init && terraform apply |
Compatibility
The provider supports Remnawave panel v2.7.x and v2.8.x. The acceptance test
suite runs against both versions in CI on every push to main and every pull
request — see the full CI matrix.
| Remnawave version | Status |
|---|---|
| v2.8.1 | ✅ Tested (primary) |
| v2.7.4 | ✅ Tested (matrix) |
The client auto-detects the backend version via /api/system/metadata on the
first API-token operation and adapts API calls where contracts differ between
versions. No configuration is required.
Quick Start
terraform {
required_providers {
remnawave = {
source = "batonogov/remnawave"
version = "~> 0.6.0"
}
}
}
provider "remnawave" {
endpoint = "https://panel.example.com"
api_token = var.remnawave_api_token
}
# ─── VPN user with 10 GB traffic limit ───
resource "remnawave_user" "john" {
username = "john-doe"
expire_at = "2027-01-01T00:00:00.000Z"
traffic_limit_bytes = 10737418240 # 10 GB
traffic_limit_strategy = "MONTH"
description = "Managed by Terraform"
}
# ─── Xray node ───
resource "remnawave_config_profile" "default" {
name = "default-profile"
config = file("xray-config.json")
}
resource "remnawave_node" "de-fra-01" {
name = "de-fra-01"
address = "1.2.3.4"
port = 443
config_profile_uuid = remnawave_config_profile.default.uuid
config_profile_inbounds = [remnawave_config_profile.default.inbounds[0].uuid]
}
# ─── Connection host ───
resource "remnawave_host" "de-fra-01-vless" {
remark = "🇩🇪 Frankfurt"
address = "vpn.example.com"
port = 443
config_profile_uuid = remnawave_config_profile.default.uuid
config_profile_inbound_uuid = remnawave_config_profile.default.inbounds[0].uuid
}
# ─── Monitor system health ───
data "remnawave_system_stats" "current" {}
output "online_users" {
value = data.remnawave_system_stats.current.online_now
}
Authentication
The provider supports two authentication methods:
API Token (recommended)
Generate a token in the panel under API Keys and pass it via the api_token
attribute or the REMNAWAVE_API_TOKEN environment variable. This is a static
JWT — no login round-trip, works with scoped permissions.
provider "remnawave" {
endpoint = "https://panel.example.com"
api_token = var.remnawave_api_token
}
Username / Password
If no API token is provided, the provider logs in via POST /api/auth/login
and obtains a JWT automatically. After a 401 response, the provider refreshes
the token and transparently replays only GET and HEAD requests. Mutating
requests fail without replay because the server may already have applied them;
the next operation authenticates with a fresh token.
provider "remnawave" {
endpoint = "https://panel.example.com"
username = "admin"
password = var.remnawave_password
}
Note: Username/password auth requires setting
proxy_headers = trueorNODE_ENV=developmenton the backend — Remnawave'sProxyCheckMiddlewarerequiresX-Forwarded-For/X-Forwarded-Protoheaders for browser-originated requests.
Reverse-Proxy Authentication
If an outer reverse proxy requires a gateway cookie or service-token headers,
pass them with the sensitive custom_headers map. For example, Cookie must
contain the complete cookie pair:
variable "remnawave_gateway_cookie" {
type = string
description = "Complete cookie pair required by the outer reverse proxy, for example cookie_name=cookie_value."
sensitive = true
}
provider "remnawave" {
endpoint = "https://panel.example.com"
api_token = var.remnawave_api_token
custom_headers = {
Cookie = var.remnawave_gateway_cookie # cookie_name=cookie_value
}
}
Alternatively, supply the map as a JSON object through the environment:
export REMNAWAVE_CUSTOM_HEADERS='{"Cookie":"cookie_name=cookie_value"}'
When custom_headers is configured in HCL, it replaces the environment map as
a whole; the two maps are not merged. Terraform's Sensitive marker only
redacts values from Terraform output. Inject header secrets through environment
variables or a secret store, and never commit secret-bearing .tfvars files.
Provider Configuration
All provider attributes can be supplied through environment variables — the provider block may be empty when the environment is configured. Explicit HCL values take precedence over environment variables.
| Attribute | Env var | Type | Description |
|---|---|---|---|
endpoint |
REMNAWAVE_ENDPOINT |
string |
Base URL of the panel, e.g. https://panel.example.com |
api_token |
REMNAWAVE_API_TOKEN |
string (sensitive) |
Pre-generated API token (JWT). If set, username/password are ignored |
username |
REMNAWAVE_USERNAME |
string |
Admin username for login |
password |
REMNAWAVE_PASSWORD |
string (sensitive) |
Admin password for login |
insecure_skip_verify |
REMNAWAVE_INSECURE_SKIP_VERIFY |
bool |
Skip TLS certificate verification (true/false) |
request_timeout |
REMNAWAVE_REQUEST_TIMEOUT |
string |
HTTP client timeout (default 30s) |
proxy_headers |
REMNAWAVE_PROXY_HEADERS |
bool |
Send X-Forwarded-For/Proto headers (bypass ProxyCheckMiddleware) |
custom_headers |
REMNAWAVE_CUSTOM_HEADERS |
map(string) (sensitive) |
Additional headers for outer reverse-proxy authentication; the environment value is a JSON object |
The client accepts at most 32 MiB of decoded data in a successful HTTP response. The limit is enforced while reading and after transparent content decompression; responses above it fail with a fixed diagnostic. HTTP error bodies have a zero-byte limit and are never read or included in diagnostics.
Resources
The provider exposes 26 resources across 7 functional areas:
Core VPN Management
| Resource | Description |
|---|---|
remnawave_user |
VPN user with traffic limits, expiration, VLESS/Trojan/Shadowsocks credentials |
remnawave_node |
Xray server node with traffic tracking, tags, consumption multipliers |
remnawave_host |
Connection endpoint (host) for VPN subscriptions |
remnawave_config_profile |
Xray config profile with inbounds, routing, sniffing |
Access Control & Routing
| Resource | Description |
|---|---|
remnawave_external_squad |
External squad (managed by name) |
remnawave_internal_squad |
Internal squad with inbounds and accessible nodes |
remnawave_subscription_template |
Subscription template (XRAY_JSON, MIHOMO, CLASH, SINGBOX, etc.) |
remnawave_subscription_settings |
Subscription page settings (singleton) |
remnawave_subpage_config |
Subscription page config (i18n, theme, blocks) |
Panel & Branding
| Resource | Description |
|---|---|
remnawave_panel_settings |
Panel branding, auth, passkey settings (singleton) |
remnawave_snippet |
Xray config snippet (reusable JSON fragments) |
remnawave_node_plugin |
Node plugin (e.g. torrent blocker) |
Infrastructure Billing
| Resource | Description |
|---|---|
remnawave_infra_provider |
Infrastructure billing provider |
remnawave_billing_node |
Infrastructure billing node (recurring billing) |
remnawave_billing_history |
Infrastructure billing history record (one-time payment) |
API & Access Tokens
| Resource | Description |
|---|---|
remnawave_api_token |
API token with scopes and expiration |
remnawave_passkey |
WebAuthn passkey (import-only — cannot be created via Terraform) |
Metadata & Devices
| Resource | Description |
|---|---|
remnawave_user_metadata |
Free-form key-value metadata for a user |
remnawave_node_metadata |
Free-form key-value metadata for a node |
remnawave_hwid_device |
HWID device entry for device-limit enforcement |
Imperative Actions
These resources trigger one-shot operations on terraform apply. Use the
triggers attribute to force re-execution when external values change.
| Resource | Description |
|---|---|
remnawave_node_action |
Enable / disable / restart / reset traffic on a node |
remnawave_user_action |
Enable / disable / reset traffic / revoke subscription on a user |
remnawave_host_bulk_action |
Bulk enable / disable / delete hosts |
remnawave_user_bulk_action |
Bulk reset traffic / revoke subscriptions / delete users / extend expiration |
remnawave_node_bulk_action |
Bulk enable / disable / restart / reset traffic on nodes |
remnawave_drop_connections |
Drop active connections by user UUID or IP address on all or selected nodes |
Data Sources
23 data sources for reading panel state:
Inventory
| Data Source | Description |
|---|---|
remnawave_nodes |
List all nodes with status and online user counts |
remnawave_users |
List all users with status and tags |
remnawave_hosts |
List all hosts |
remnawave_config_profiles |
List all config profiles |
remnawave_host_tags |
List all unique host tags |
remnawave_passkeys |
List WebAuthn passkeys for the current admin |
System & Health
| Data Source | Description |
|---|---|
remnawave_system_health |
Panel system health (raw JSON) |
remnawave_system_stats |
CPU, memory, uptime, user status counts, online stats |
remnawave_system_recap |
Monthly/total summary: users, traffic, nodes, version |
remnawave_system_bandwidth_stats |
System-level bandwidth statistics |
remnawave_system_nodes_stats |
System-level per-node statistics |
Bandwidth & Metrics
| Data Source | Description |
|---|---|
remnawave_nodes_metrics |
Per-node live metrics: users online, inbounds/outbounds |
remnawave_bandwidth_stats |
Per-node bandwidth usage by date range |
remnawave_bandwidth_stats_user |
Per-user bandwidth usage by date range |
remnawave_bandwidth_realtime |
Realtime bandwidth metrics per node |
Subscriptions
| Data Source | Description |
|---|---|
remnawave_subscriptions |
Fetch subscription by UUID/username/short UUID |
remnawave_subscription_request_history |
Subscription request history |
remnawave_subscription_request_history_stats |
Subscription request statistics |
remnawave_connection_keys |
Per-protocol connection keys for a user |
Other
| Data Source | Description |
|---|---|
remnawave_keygen |
Panel public key for node setup |
remnawave_user_ips |
Fetch IPs a user is currently connected from |
remnawave_hwid_stats |
HWID device statistics |
remnawave_hwid_top_users |
Top users by HWID device count |
Examples
Browse the examples/ directory for focused configuration
examples. Some snippets reference resources or variables supplied by the
surrounding configuration:
examples/
├── resources/
│ ├── remnawave_user/ # User with traffic limit + tag
│ ├── remnawave_node_action/ # Periodic traffic reset via triggers
│ ├── remnawave_host_bulk_action/# Bulk enable/disable/delete
│ ├── remnawave_user_bulk_action/# Bulk user operations
│ ├── remnawave_node_bulk_action/# Bulk node operations
│ ├── remnawave_user_action/ # Enable/disable/reset/revoke
│ └── remnawave_passkey/ # Import + manage existing passkeys
└── data-sources/
├── remnawave_users/ # List all users
├── remnawave_system_stats/ # Dashboard metrics
└── remnawave_passkeys/ # List passkeys
Full documentation for every resource and data source is on the
Terraform Registry
and in docs/.
Importing Existing Resources
Resources created outside Terraform (e.g. via the web UI) can be imported into Terraform state:
# Import a user by UUID
terraform import remnawave_user.john 550e8400-e29b-41d4-a716-446655440000
# Import a node by UUID
terraform import remnawave_node.de-fra-01 550e8400-e29b-41d4-a716-446655440001
# Import a passkey (import-only resource)
terraform import remnawave_passkey.admin 550e8400-e29b-41d4-a716-446655440002
Import IDs are resource-specific. Most stateful resources use a UUID, snippets use their name, and resources with compound identities document their required format on the corresponding resource page.
Versioning & Upgrades
This provider follows Semantic Versioning. Breaking
changes are reserved for major releases (e.g. 1.0.0).
Upgrading
# Upgrade to the latest version
terraform init -upgrade
Review the CHANGELOG for breaking changes and migration notes before upgrading across major versions.
Version Constraints
terraform {
required_providers {
remnawave = {
source = "batonogov/remnawave"
version = "~> 0.6.0" # Allow 0.6.x patch releases
}
}
}
Release Integrity
Each platform archive is covered by signed checksums, a matching SPDX 2.3 SBOM, and GitHub/Sigstore build provenance bound to its SHA-256 digest and the release workflow. See Verifying Releases for commands that validate the archive, SBOM, source commit, and workflow identity.
Development
Requirements
| Tool | Version |
|---|---|
| Go | 1.26.5+ (see go.mod) |
| Terraform CLI | 1.12+ |
| Task | Latest (optional, for task commands) |
| Docker | Required for acceptance tests |
Build & Test
# Build the provider binary
go build -o terraform-provider-remnawave
# Format code
gofmt -w provider/*.go
# Lint
golangci-lint run
# Unit tests (no Docker needed)
go test ./provider/... -race -cover
# Reachable vulnerability scan
task test:vuln
# Acceptance tests (starts Remnawave panel via Docker Compose)
task test:acc
See Taskfile.yml and AGENTS.md for the full
list of commands and development conventions.
Acceptance Tests
Acceptance tests run against a real Remnawave panel via Docker Compose
(docker-compose.yaml). The compose file spins up:
remnawave/backendpanel (port 3000)- PostgreSQL 18
- Valkey (Redis) 9
# Run the complete Docker lifecycle + test suite
task test:acc
# Override the Remnawave version under test
REMNAWAVE_VERSION=2.9.0 REMNAWAVE_DIGEST=sha256:<digest> task test:acc
All compose images are pinned by sha256 digest for reproducibility.
Regenerating Documentation
Documentation is auto-generated from provider schemas via
tfplugindocs:
task docs # Format examples + regenerate Registry docs
task docs:check # Validate and detect stale generated docs (CI gate)
Contributing
Contributions are welcome! Please:
- Open an issue to discuss the change before starting work.
- Follow Conventional Commits for
commit messages (
feat:,fix:,docs:,test:,chore:). - Add or update tests for any changed behavior.
- Run
gofmt,golangci-lint, and the relevant test suite before submitting a pull request. - Ensure CI is green — acceptance tests run against both Remnawave v2.7.4 and v2.8.1.
Releases are machine-gated: the release workflow runs only after successful CI
for the exact current main commit, then verifies the CI jobs, tag target, and
embedded Go VCS revision before publishing.
Security
If you believe you have found a security issue, please report it privately rather than opening a public issue.
License
MIT © 2026 Fedor Batonogov
Documentation
¶
There is no documentation for this package.