1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00

fix(gstfsd): only the panel, with the host's key, can change a VM's disk

gstfsd, the root service on each compute that rewrites a shut-off VM's
/etc/shadow and authorized_keys, did what any client on its port asked.
Now it needs GSTFSD_KEY (64 hexadecimal characters, in the unit's
/etc/default/webvirt-gstfsd) and does not start without it. Each
connection gets a new challenge; the panel signs its request for it
with HMAC-SHA256 and gstfsd checks the signature before any change, so
a request recorded on one connection is refused on another. gstfsd
signs its reply for the same challenge and the panel's nonce, and the
panel accepts only that reply.

Each compute has a gstfsd key, the host's GSTFSD_KEY: the compute forms
and the API take it and never show it, and an empty value on an edit
keeps it. The bootstrap script makes the key file once, readable by
root only; the README shows how to make it and where the panel takes
it. The traffic is still not encrypted: a remote panel's port stays
open to the panel's address only.
This commit is contained in:
catborise 2026-10-31 01:53:43 +03:00
parent a408cfdb2b
commit 2dab23cd9e
13 changed files with 388 additions and 108 deletions

View file

@ -7,47 +7,55 @@ from computes.models import Compute
from .validators import validate_hostname
class KeepPasswordMixin:
"""The form never shows the stored password: left empty on an edit, it
stays as it is."""
class KeepSecretsMixin:
"""The form never shows the stored password or gstfsd key: left empty on
an edit, they stay as they are."""
def clean_password(self):
return self.cleaned_data["password"] or self.instance.password
def clean_gstfsd_key(self):
return self.cleaned_data["gstfsd_key"] or self.instance.gstfsd_key
class TcpComputeForm(KeepPasswordMixin, forms.ModelForm):
SECRET_WIDGETS = {"password": forms.PasswordInput(), "gstfsd_key": forms.PasswordInput()}
class TcpComputeForm(KeepSecretsMixin, forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname])
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
class Meta:
model = Compute
widgets = {"password": forms.PasswordInput()}
widgets = SECRET_WIDGETS
fields = "__all__"
class SshComputeForm(forms.ModelForm):
class SshComputeForm(KeepSecretsMixin, forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
class Meta:
model = Compute
widgets = SECRET_WIDGETS
exclude = ["password"]
class TlsComputeForm(KeepPasswordMixin, forms.ModelForm):
class TlsComputeForm(KeepSecretsMixin, forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname])
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
class Meta:
model = Compute
widgets = {"password": forms.PasswordInput()}
widgets = SECRET_WIDGETS
fields = "__all__"
class SocketComputeForm(forms.ModelForm):
class SocketComputeForm(KeepSecretsMixin, forms.ModelForm):
hostname = forms.CharField(widget=forms.HiddenInput, initial="localhost")
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
class Meta:
model = Compute
fields = ["name", "details", "hostname", "type", "migration_address"]
widgets = SECRET_WIDGETS
fields = ["name", "details", "hostname", "type", "migration_address", "gstfsd_key"]