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
- 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.0 | ✅ 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.4.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
inbound = {
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. The token is refreshed on 401 responses.
provider "remnawave" {
endpoint = "https://panel.example.com"
username = "admin"
password = var.remnawave_password
}
Note: Username/password auth requires setting
proxy_headers = trueorNODE_ENV=devon the backend — Remnawave'sProxyCheckMiddlewarerequiresX-Forwarded-For/X-Forwarded-Protoheaders for browser-originated requests.
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) |
Resources
The provider exposes 24 resources across 6 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_drop_connections |
Drop active connections for a user |
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 complete, copy-paste-ready
configurations:
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_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
All resources that support import accept the UUID as the import ID.
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.4" # Allow 0.4.x patches
}
}
}
Development
Requirements
| Tool | Version |
|---|---|
| Go | 1.24+ (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
# 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.0.
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.