mirror of
https://gitlab.com/ArkHost/WHMCS-ArkHost-HetznerVPS.git
synced 2026-07-24 15:55:53 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9049937d8e | ||
|
|
fb4e94b086 | ||
|
|
3bfc327763 | ||
|
|
8b7d6b2c62 | ||
|
|
3c526260cd | ||
|
|
8d4673378c | ||
|
|
5421cd0b30 | ||
|
|
8887733e0e | ||
|
|
af56d91bff | ||
|
|
60fb7b0ce0 | ||
|
|
64dde17f0e | ||
|
|
6b29ab199c | ||
|
|
ff62c3a64c | ||
|
|
0de4798583 | ||
|
|
ef4c336049 | ||
|
|
f7df8b6492 | ||
|
|
7a846f7fba | ||
|
|
332e21970d | ||
|
|
394f7e8af8 | ||
|
|
16f72f786c | ||
|
|
4181c45085 | ||
|
|
ff492c7d85 |
@@ -12,7 +12,6 @@ WHMCS server module for Hetzner Cloud VPS management.
|
|||||||
|
|
||||||
**Advanced**
|
**Advanced**
|
||||||
|
|
||||||
- Cloud-init support for automated server configuration
|
|
||||||
- Floating IP management with reverse DNS
|
- Floating IP management with reverse DNS
|
||||||
- Backup creation and restoration
|
- Backup creation and restoration
|
||||||
- Firewall rule management
|
- Firewall rule management
|
||||||
@@ -56,12 +55,11 @@ WHMCS server module for Hetzner Cloud VPS management.
|
|||||||
|
|
||||||
**Module Settings**
|
**Module Settings**
|
||||||
|
|
||||||
- Server Type: `cx23`, `cx33`, `cx43`, `cx53` (Cost-Optimized), `cpx11`, `cpx21`, `cax11`, `ccx13`, etc.
|
- Server Type: `cx11`, `cx21`, `cx31`, `cx41`, `cx51` , etc.
|
||||||
- Operating System: `ubuntu-22.04`, `debian-11`, `rocky-9`, etc.
|
- Operating System: `ubuntu-20.04`, `debian-11`, etc.
|
||||||
- Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`, `sin`
|
- Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`
|
||||||
- Backups: On/Off
|
- Backups: On/Off
|
||||||
- Create Floating IP: On/Off
|
- Create Floating IP: On/Off
|
||||||
- Cloud-Init YAML: Optional custom cloud-init configuration
|
|
||||||
|
|
||||||
**Custom Field (Required)**
|
**Custom Field (Required)**
|
||||||
|
|
||||||
@@ -100,10 +98,6 @@ Create in Setup → Products/Services → Configurable Options:
|
|||||||
- Option Name: `planid`
|
- Option Name: `planid`
|
||||||
- Option Type: `Dropdown`
|
- Option Type: `Dropdown`
|
||||||
- Options:
|
- Options:
|
||||||
- `cx23|CX23 - 2 vCPU, 4 GB RAM, 40 GB Disk, shared (Cost-Optimized)`
|
|
||||||
- `cx33|CX33 - 4 vCPU, 8 GB RAM, 80 GB Disk, shared (Cost-Optimized)`
|
|
||||||
- `cx43|CX43 - 8 vCPU, 16 GB RAM, 160 GB Disk, shared (Cost-Optimized)`
|
|
||||||
- `cx53|CX53 - 16 vCPU, 32 GB RAM, 320 GB Disk, shared (Cost-Optimized)`
|
|
||||||
- `cpx11|CPX 11 - 2 vCPU, 2 GB RAM, 40 GB Disk, shared`
|
- `cpx11|CPX 11 - 2 vCPU, 2 GB RAM, 40 GB Disk, shared`
|
||||||
- `cpx21|CPX 21 - 3 vCPU, 4 GB RAM, 80 GB Disk, shared`
|
- `cpx21|CPX 21 - 3 vCPU, 4 GB RAM, 80 GB Disk, shared`
|
||||||
- `cpx31|CPX 31 - 4 vCPU, 8 GB RAM, 160 GB Disk, shared`
|
- `cpx31|CPX 31 - 4 vCPU, 8 GB RAM, 160 GB Disk, shared`
|
||||||
@@ -127,36 +121,34 @@ Create in Setup → Products/Services → Configurable Options:
|
|||||||
|
|
||||||
**4\. Operating System**
|
**4\. Operating System**
|
||||||
|
|
||||||
- Option Name: `osid`
|
- Option Name: `osid`
|
||||||
- Option Type: `Dropdown`
|
- Option Type: `Dropdown`
|
||||||
- Options:
|
- Options:
|
||||||
|
- `lamp|lamp`
|
||||||
|
-`wordpress|wordpress`
|
||||||
|
-`jitsi|jitsi`
|
||||||
|
-`nextcloud|nextcloud`
|
||||||
|
- `docker-ce|docker-ce`
|
||||||
|
- `gitlab|gitlab`
|
||||||
- `debian-11|Debian 11`
|
- `debian-11|Debian 11`
|
||||||
- `debian-12|Debian 12`
|
- `rocky-8|Rocky Linux 8`
|
||||||
- `debian-13|Debian 13`
|
- `centos-stream-9|CentOS Stream 9`
|
||||||
- `ubuntu-22.04|Ubuntu 22.04`
|
- `ubuntu-22.04|Ubuntu 22.04`
|
||||||
- `ubuntu-24.04|Ubuntu 24.04`
|
- `prometheus-grafana|prometheus-grafana`
|
||||||
|
- `rocky-9|Rocky Linux 9`
|
||||||
|
- `wireguard|wireguard`
|
||||||
|
- `owncast|owncast`
|
||||||
|
- `photoprism|photoprism`
|
||||||
|
- `rustdesk|rustdesk`
|
||||||
|
- `centos-stream-9|CentOS Stream 9`
|
||||||
|
- `debian-11|Debian 11`
|
||||||
- `rocky-8|Rocky Linux 8`
|
- `rocky-8|Rocky Linux 8`
|
||||||
- `rocky-9|Rocky Linux 9`
|
- `rocky-9|Rocky Linux 9`
|
||||||
- `rocky-10|Rocky Linux 10`
|
- `ubuntu-22.04|Ubuntu 22.04`
|
||||||
- `alma-8|AlmaLinux 8`
|
- `alma-8|AlmaLinux 8`
|
||||||
- `alma-9|AlmaLinux 9`
|
- `alma-9|AlmaLinux 9`
|
||||||
- `alma-10|AlmaLinux 10`
|
- `jitsi|jitsi`
|
||||||
- `centos-stream-9|CentOS Stream 9`
|
- `owncast|owncast`
|
||||||
- `centos-stream-10|CentOS Stream 10`
|
|
||||||
- `opensuse-15|openSUSE 15`
|
|
||||||
- `fedora-41|Fedora 41`
|
|
||||||
- `fedora-42|Fedora 42`
|
|
||||||
- `docker-ce|Docker CE`
|
|
||||||
- `lamp|LAMP Stack`
|
|
||||||
- `wordpress|WordPress`
|
|
||||||
- `nextcloud|Nextcloud`
|
|
||||||
- `gitlab|GitLab`
|
|
||||||
- `jitsi|Jitsi`
|
|
||||||
- `wireguard|WireGuard`
|
|
||||||
- `prometheus-grafana|Prometheus + Grafana`
|
|
||||||
- `owncast|Owncast`
|
|
||||||
- `photoprism|PhotoPrism`
|
|
||||||
- `rustdesk|RustDesk`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -191,102 +183,6 @@ This ensures proper key isolation and security for each customer.
|
|||||||
|
|
||||||
We're exploring secure implementations using cloud-init or per-server user data to enable SSH key management while maintaining proper isolation.
|
We're exploring secure implementations using cloud-init or per-server user data to enable SSH key management while maintaining proper isolation.
|
||||||
|
|
||||||
## Cloud-Init Support
|
|
||||||
|
|
||||||
Cloud-init allows automatic server configuration during first boot. Unlike project-level SSH keys, cloud-init user_data is per-server, ensuring proper isolation in multi-tenant environments.
|
|
||||||
|
|
||||||
**Configuration:**
|
|
||||||
|
|
||||||
1. Navigate to Setup → Products/Services → Products/Services
|
|
||||||
2. Edit your product → Module Settings tab
|
|
||||||
3. Find "Cloud-Init YAML (Optional)" textarea
|
|
||||||
4. Enter your cloud-init configuration in YAML format
|
|
||||||
5. Leave empty to skip cloud-init
|
|
||||||
|
|
||||||
**Common Use Cases:**
|
|
||||||
|
|
||||||
**1. Change APT Mirrors** (original use case - avoid Hetzner mirrors):
|
|
||||||
```yaml
|
|
||||||
#cloud-config
|
|
||||||
apt:
|
|
||||||
primary:
|
|
||||||
- arches: [default]
|
|
||||||
uri: http://de.archive.ubuntu.com/ubuntu/
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Add SSH Keys** (secure alternative to project-level keys):
|
|
||||||
```yaml
|
|
||||||
#cloud-config
|
|
||||||
users:
|
|
||||||
- name: admin
|
|
||||||
ssh_authorized_keys:
|
|
||||||
- ssh-rsa AAAAB3NzaC1yc2E... user@laptop
|
|
||||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
||||||
shell: /bin/bash
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Install Docker:**
|
|
||||||
```yaml
|
|
||||||
#cloud-config
|
|
||||||
packages:
|
|
||||||
- docker.io
|
|
||||||
- docker-compose
|
|
||||||
|
|
||||||
runcmd:
|
|
||||||
- systemctl enable docker
|
|
||||||
- systemctl start docker
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Security Hardening:**
|
|
||||||
```yaml
|
|
||||||
#cloud-config
|
|
||||||
packages:
|
|
||||||
- fail2ban
|
|
||||||
- ufw
|
|
||||||
|
|
||||||
runcmd:
|
|
||||||
- ufw default deny incoming
|
|
||||||
- ufw default allow outgoing
|
|
||||||
- ufw allow 22/tcp
|
|
||||||
- ufw --force enable
|
|
||||||
- systemctl enable fail2ban
|
|
||||||
- systemctl start fail2ban
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Combined Configuration:**
|
|
||||||
```yaml
|
|
||||||
#cloud-config
|
|
||||||
apt:
|
|
||||||
primary:
|
|
||||||
- arches: [default]
|
|
||||||
uri: http://mirror.example.com/ubuntu/
|
|
||||||
|
|
||||||
users:
|
|
||||||
- name: admin
|
|
||||||
ssh_authorized_keys:
|
|
||||||
- ssh-rsa AAAAB3... admin@company
|
|
||||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
||||||
|
|
||||||
packages:
|
|
||||||
- fail2ban
|
|
||||||
- ufw
|
|
||||||
- docker.io
|
|
||||||
|
|
||||||
runcmd:
|
|
||||||
- ufw allow 22/tcp
|
|
||||||
- ufw --force enable
|
|
||||||
- systemctl enable docker fail2ban
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important Notes:**
|
|
||||||
|
|
||||||
- YAML must be valid syntax (use a YAML validator if unsure)
|
|
||||||
- If you don't start with `#cloud-config`, the module will add it automatically
|
|
||||||
- Cloud-init runs ONCE on first boot only
|
|
||||||
- Invalid YAML will cause provisioning to fail silently
|
|
||||||
- Maximum size: 32 KiB (Hetzner API limit)
|
|
||||||
- Documentation: https://docs.hetzner.cloud/#servers-create-a-server
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||

|

|
||||||

|

|
||||||
@@ -338,3 +234,5 @@ Free and open source. Commercial support available.
|
|||||||
Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://arkhost.com/whmcs-modules.php "https://arkhost.com/whmcs-modules.php")
|
Check out our other WHMCS modules at [arkhost.com/whmcs-modules.php](https://arkhost.com/whmcs-modules.php "https://arkhost.com/whmcs-modules.php")
|
||||||
|
|
||||||
© 2025 ArkHost
|
© 2025 ArkHost
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|||||||
@@ -116,12 +116,12 @@ function ArkHostHetznerVPS_API(array $params) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Packages':
|
case 'Packages':
|
||||||
$url .= 'server_types?per_page=50';
|
$url .= 'server_types';
|
||||||
$method = 'GET';
|
$method = 'GET';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Operating Systems':
|
case 'Operating Systems':
|
||||||
$url .= 'images?type=system&per_page=50';
|
$url .= 'images?type=system';
|
||||||
$method = 'GET';
|
$method = 'GET';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -177,20 +177,6 @@ function ArkHostHetznerVPS_API(array $params) {
|
|||||||
$data['enable_ipv6'] = false;
|
$data['enable_ipv6'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Cloud-Init user_data if provided
|
|
||||||
$cloudInitYaml = ArkHostHetznerVPS_GetOption($params, 'cloud_init_yaml');
|
|
||||||
if ($cloudInitYaml && trim($cloudInitYaml) !== '') {
|
|
||||||
// Basic YAML validation - check if it starts with #cloud-config
|
|
||||||
$trimmedYaml = trim($cloudInitYaml);
|
|
||||||
if (strpos($trimmedYaml, '#cloud-config') !== 0) {
|
|
||||||
// Auto-prepend #cloud-config if missing
|
|
||||||
$cloudInitYaml = "#cloud-config\n" . $cloudInitYaml;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass as plain text - Hetzner API accepts user_data as plain string (max 32KiB)
|
|
||||||
$data['user_data'] = $cloudInitYaml;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle SSH keys from custom field if needed
|
// Handle SSH keys from custom field if needed
|
||||||
// This would need to be implemented with a custom field
|
// This would need to be implemented with a custom field
|
||||||
break;
|
break;
|
||||||
@@ -253,7 +239,7 @@ function ArkHostHetznerVPS_API(array $params) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Operating Systems - Server':
|
case 'Operating Systems - Server':
|
||||||
$url .= 'images?type=system&per_page=50';
|
$url .= 'images?type=system';
|
||||||
$method = 'GET';
|
$method = 'GET';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1002,13 +988,6 @@ function ArkHostHetznerVPS_ConfigOptions() {
|
|||||||
'Description' => 'Automatically create a floating IP when provisioning this server (additional cost - billed by Hetzner).',
|
'Description' => 'Automatically create a floating IP when provisioning this server (additional cost - billed by Hetzner).',
|
||||||
'Type' => 'yesno',
|
'Type' => 'yesno',
|
||||||
),
|
),
|
||||||
'cloud_init_yaml' => array(
|
|
||||||
'FriendlyName' => 'Cloud-Init YAML (Optional)',
|
|
||||||
'Description' => 'Custom cloud-init configuration in YAML format (max 32KiB). Passed as user_data to Hetzner API during server creation. Leave empty to skip cloud-init. <a href="https://docs.hetzner.cloud/#servers-create-a-server" target="_blank">Documentation</a>',
|
|
||||||
'Type' => 'textarea',
|
|
||||||
'Rows' => '10',
|
|
||||||
'Cols' => '60',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1181,11 +1160,6 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
|
|||||||
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
||||||
'password' => encrypt($create['root_password'])
|
'password' => encrypt($create['root_password'])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Save the timestamp when password was set for expiration tracking (72 hours)
|
|
||||||
$params['model']->serviceProperties->save([
|
|
||||||
'ArkHostHetznerVPS|Password Set Time' => time()
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle floating IP creation if requested via Configurable Options or Module Settings
|
// Handle floating IP creation if requested via Configurable Options or Module Settings
|
||||||
@@ -1931,11 +1905,6 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
|||||||
->where('id', $params['serviceid'])
|
->where('id', $params['serviceid'])
|
||||||
->update(['password' => encrypt($result['root_password'])]);
|
->update(['password' => encrypt($result['root_password'])]);
|
||||||
|
|
||||||
// Save the timestamp when password was set for expiration tracking (72 hours)
|
|
||||||
$params['model']->serviceProperties->save([
|
|
||||||
'ArkHostHetznerVPS|Password Set Time' => time()
|
|
||||||
]);
|
|
||||||
|
|
||||||
$results['root_password'] = $result['root_password'];
|
$results['root_password'] = $result['root_password'];
|
||||||
$results['message'] = 'Server rebuild initiated. New root password has been saved.';
|
$results['message'] = 'Server rebuild initiated. New root password has been saved.';
|
||||||
} else {
|
} else {
|
||||||
@@ -1972,21 +1941,6 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
|
|||||||
$results['result'] = 'error';
|
$results['result'] = 'error';
|
||||||
$results['message'] = 'Invalid floating IP ID for this service';
|
$results['message'] = 'Invalid floating IP ID for this service';
|
||||||
}
|
}
|
||||||
} else if ($action === 'Reset root') {
|
|
||||||
// Handle password reset from client area
|
|
||||||
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
|
|
||||||
|
|
||||||
// Store the new root password and timestamp if provided
|
|
||||||
if (isset($result['root_password'])) {
|
|
||||||
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
|
||||||
'password' => encrypt($result['root_password'])
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Save the timestamp when password was set for expiration tracking (72 hours)
|
|
||||||
$params['model']->serviceProperties->save([
|
|
||||||
'ArkHostHetznerVPS|Password Set Time' => time()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
|
$results = array_merge($results, is_array($result) ? $result : array('data' => $result));
|
||||||
}
|
}
|
||||||
@@ -2393,26 +2347,6 @@ function ArkHostHetznerVPS_ClientArea(array $params) {
|
|||||||
$serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A';
|
$serverInfo['datacenter'] = isset($serverInfo['datacenter']['description']) ? $serverInfo['datacenter']['description'] : 'N/A';
|
||||||
$serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A';
|
$serverInfo['location'] = isset($serverInfo['datacenter']['location']['city']) ? $serverInfo['datacenter']['location']['city'] : 'N/A';
|
||||||
|
|
||||||
// Get root password with expiration check (72 hours)
|
|
||||||
$passwordSetTime = $params['model']->serviceProperties->get('ArkHostHetznerVPS|Password Set Time');
|
|
||||||
$currentTime = time();
|
|
||||||
$expirationPeriod = 72 * 3600; // 72 hours in seconds
|
|
||||||
|
|
||||||
if (!empty($params['password']) && $params['password'] !== 'managed-via-api') {
|
|
||||||
// If no timestamp exists, show the password (backward compatibility or initial setup)
|
|
||||||
// If timestamp exists, check if it's within the 72-hour window
|
|
||||||
if (!$passwordSetTime || ($currentTime - $passwordSetTime) < $expirationPeriod) {
|
|
||||||
// Password is still valid - WHMCS already decrypts it for us in $params
|
|
||||||
// No need to call decrypt() as $params['password'] is already plain text
|
|
||||||
$serverInfo['install_root'] = $params['password'];
|
|
||||||
} else {
|
|
||||||
// Password has expired (timestamp exists and is older than 72 hours)
|
|
||||||
$serverInfo['install_root'] = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$serverInfo['install_root'] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, always show backups tab - the API will handle permissions
|
// For now, always show backups tab - the API will handle permissions
|
||||||
$backupsEnabled = true;
|
$backupsEnabled = true;
|
||||||
|
|
||||||
@@ -2508,11 +2442,6 @@ function ArkHostHetznerVPS_ResetRoot(array $params) {
|
|||||||
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
Capsule::table('tblhosting')->where('id', $params['serviceid'])->update([
|
||||||
'password' => encrypt($result['root_password'])
|
'password' => encrypt($result['root_password'])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Save the timestamp when password was set for expiration tracking (72 hours)
|
|
||||||
$params['model']->serviceProperties->save([
|
|
||||||
'ArkHostHetznerVPS|Password Set Time' => time()
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
} catch (Exception $err) {
|
} catch (Exception $err) {
|
||||||
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
|
ArkHostHetznerVPS_Error(__FUNCTION__, $params, $err);
|
||||||
|
|||||||
@@ -224,12 +224,6 @@ function resetRootPassword() {
|
|||||||
function() {
|
function() {
|
||||||
ArkHostHetznerVPS_API('Reset root', true, {}, function(data) {
|
ArkHostHetznerVPS_API('Reset root', true, {}, function(data) {
|
||||||
if (data.root_password) {
|
if (data.root_password) {
|
||||||
// Update the password field on the page
|
|
||||||
var passwordField = document.getElementById('vpsPassword');
|
|
||||||
if (passwordField) {
|
|
||||||
passwordField.value = data.root_password;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show password in a special modal or notification
|
// Show password in a special modal or notification
|
||||||
var passwordHtml = '<div class="alert alert-success">' +
|
var passwordHtml = '<div class="alert alert-success">' +
|
||||||
'<h5>' + lang.newRootPassword + '</h5>' +
|
'<h5>' + lang.newRootPassword + '</h5>' +
|
||||||
|
|||||||
Reference in New Issue
Block a user