3 Commits
Author SHA1 Message Date
Yuri bc9d8a2e1c per product cloud-init optional support 2025-11-03 15:07:02 +01:00
Yuri 0f96849032 add new server plans and os versions, fix api pagination 2025-11-03 14:19:57 +01:00
Yuri cc60ca7103 password fix 2025-11-03 13:40:33 +01:00
4 changed files with 214 additions and 35 deletions
+1 -1
View File
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
+128 -26
View File
@@ -12,6 +12,7 @@ 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
@@ -55,11 +56,12 @@ WHMCS server module for Hetzner Cloud VPS management.
**Module Settings** **Module Settings**
- Server Type: `cx11`, `cx21`, `cx31`, `cx41`, `cx51` , etc. - Server Type: `cx23`, `cx33`, `cx43`, `cx53` (Cost-Optimized), `cpx11`, `cpx21`, `cax11`, `ccx13`, etc.
- Operating System: `ubuntu-20.04`, `debian-11`, etc. - Operating System: `ubuntu-22.04`, `debian-11`, `rocky-9`, etc.
- Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil` - Datacenter: `fsn1`, `nbg1`, `hel1`, `ash`, `hil`, `sin`
- 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)**
@@ -98,6 +100,10 @@ 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`
@@ -121,34 +127,36 @@ 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`
- `rocky-8|Rocky Linux 8` - `debian-12|Debian 12`
- `centos-stream-9|CentOS Stream 9` - `debian-13|Debian 13`
- `ubuntu-22.04|Ubuntu 22.04` - `ubuntu-22.04|Ubuntu 22.04`
- `prometheus-grafana|prometheus-grafana` - `ubuntu-24.04|Ubuntu 24.04`
- `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`
- `ubuntu-22.04|Ubuntu 22.04` - `rocky-10|Rocky Linux 10`
- `alma-8|AlmaLinux 8` - `alma-8|AlmaLinux 8`
- `alma-9|AlmaLinux 9` - `alma-9|AlmaLinux 9`
- `jitsi|jitsi` - `alma-10|AlmaLinux 10`
- `owncast|owncast` - `centos-stream-9|CentOS Stream 9`
- `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`
@@ -183,6 +191,102 @@ 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
![Screenshot 1](screenshots/1.png) ![Screenshot 1](screenshots/1.png)
![Screenshot 2](screenshots/2.png) ![Screenshot 2](screenshots/2.png)
@@ -234,5 +338,3 @@ 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'; $url .= 'server_types?per_page=50';
$method = 'GET'; $method = 'GET';
break; break;
case 'Operating Systems': case 'Operating Systems':
$url .= 'images?type=system'; $url .= 'images?type=system&per_page=50';
$method = 'GET'; $method = 'GET';
break; break;
@@ -176,7 +176,21 @@ function ArkHostHetznerVPS_API(array $params) {
if (ArkHostHetznerVPS_GetOption($params, 'ipv6') === 'on') { if (ArkHostHetznerVPS_GetOption($params, 'ipv6') === 'on') {
$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;
@@ -239,7 +253,7 @@ function ArkHostHetznerVPS_API(array $params) {
break; break;
case 'Operating Systems - Server': case 'Operating Systems - Server':
$url .= 'images?type=system'; $url .= 'images?type=system&per_page=50';
$method = 'GET'; $method = 'GET';
break; break;
@@ -988,6 +1002,13 @@ 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 {
@@ -1154,12 +1175,17 @@ function ArkHostHetznerVPS_CreateAccount(array $params) {
$params['model']->serviceProperties->save([ $params['model']->serviceProperties->save([
'ArkHostHetznerVPS|VPS ID' => $create['server']['id'], 'ArkHostHetznerVPS|VPS ID' => $create['server']['id'],
]); ]);
// Store the root password if provided // Store the root password if provided
if (isset($create['root_password'])) { if (isset($create['root_password'])) {
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
@@ -1904,7 +1930,12 @@ function ArkHostHetznerVPS_ClientAreaAPI(array $params) {
Capsule::table('tblhosting') Capsule::table('tblhosting')
->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 {
@@ -1941,6 +1972,21 @@ 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));
} }
@@ -2347,6 +2393,26 @@ 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;
@@ -2436,12 +2502,17 @@ function ArkHostHetznerVPS_ResetRoot(array $params) {
try { try {
$params['action'] = 'Reset root'; $params['action'] = 'Reset root';
$result = ArkHostHetznerVPS_API($params); $result = ArkHostHetznerVPS_API($params);
// Store the new root password if provided // Store the new root password if provided
if (isset($result['root_password'])) { if (isset($result['root_password'])) {
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,6 +224,12 @@ 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>' +
@@ -236,7 +242,7 @@ function resetRootPassword() {
'</div>' + '</div>' +
'<p class="mt-2 mb-0"><small>' + lang.savePassword + ' ' + lang.passwordSaved + '</small></p>' + '<p class="mt-2 mb-0"><small>' + lang.savePassword + ' ' + lang.passwordSaved + '</small></p>' +
'</div>'; '</div>';
// Create a modal to show the password // Create a modal to show the password
var modal = document.createElement('div'); var modal = document.createElement('div');
modal.className = 'arkhost-confirm-overlay show'; modal.className = 'arkhost-confirm-overlay show';