Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0


## [Unreleased]
### Fixed
- [SYSTEM] [PKI] Broken form


## [2.27.1] - 2025-06-13
Expand Down
49 changes: 30 additions & 19 deletions vulture_os/system/pki/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
# Django system imports
from django.conf import settings
from django.forms import (ModelChoiceField, ModelForm, Select, SelectMultiple, TextInput, Textarea, ValidationError,
CharField, ChoiceField, RadioSelect)
from system.pki.models import (ALPN_CHOICES, BROWSER_CHOICES, PROTOCOL_CHOICES, TLSProfile, X509Certificate,
VERIFY_CHOICES)
CharField, ChoiceField)
from system.pki.models import (TLSProfile, X509Certificate, ALPN_CHOICES, BROWSER_CHOICES, PROTOCOL_CHOICES,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no change

VERIFY_CHOICES, CERTIFICATE_TYPE_CHOICES)

from ast import literal_eval
from cryptography import x509
Expand All @@ -39,35 +39,46 @@
logger = logging.getLogger('gui')


class X509InternalCertificateForm(ModelForm):
class X509CertificateForm(ModelForm):

cn = CharField(required=True)
type = ChoiceField(required=True,choices=(('internal','Self-Signed Vulture Certificate'),('letsencrypt','Let\'s Encrypt Certificate'),('external','External certificate')))
cn = CharField(widget=TextInput(attrs={'class': 'form-control'}))
type = ChoiceField(
required=True,
choices=CERTIFICATE_TYPE_CHOICES,
widget=Select(attrs={'class': 'form-control select2'})
)

class Meta:
model = X509Certificate
fields = ('name','cn','type',)
fields = ('name', 'cn', 'type')

widgets = {
'name': TextInput(attrs={'class': 'form-control'}),
'type': RadioSelect(choices=(('internal','Self-Signed Vulture Certificate'),('letsencrypt','Let\'s Encrypt Certificate'),('external','External certificate')), attrs={'class': 'form-control select2'}),
'cn': TextInput(attrs={'class': 'form-control'})
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
if self.instance.is_external:
self.fields['type'].choices = ((k,v) for k,v in CERTIFICATE_TYPE_CHOICES if k == "external")
else:
self.fields['type'].choices = ((k,v) for k,v in CERTIFICATE_TYPE_CHOICES if k == "internal")
self.fields['cn'].disabled = True

class X509ExternalCertificateForm(ModelForm):
def clean(self):
""" Verify if cn is filled-in if type is internal """
cleaned_data = super().clean()
if cleaned_data.get('type') == "internal" and not cleaned_data.get('cn'):
self.add_error('cn', "This field is required if 'type' is 'internal'")
Comment on lines +71 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if cleaned_data.get('type') == "internal" and not cleaned_data.get('cn'):
self.add_error('cn', "This field is required if 'type' is 'internal'")
if cleaned_data.get('type') != "external" and not cleaned_data.get('cn'):
self.add_error('cn', "This field is required")

return cleaned_data

cn = CharField(required=False)
type = ChoiceField(required=True,choices=(('internal','Self-Signed Vulture Certificate'),('letsencrypt','Let\'s Encrypt Certificate'),('external','External certificate')))

class Meta:
model = X509Certificate
fields = ('name', 'type', 'cert', 'key', 'chain', 'crl', 'crl_uri')
class X509ExternalCertificateForm(X509CertificateForm):

widgets = {
'name': TextInput(attrs={'class': 'form-control'}),
'type': RadioSelect(choices=(('internal','Self-Signed Vulture Certificate'),('letsencrypt','Let\'s Encrypt Certificate'),('external','External certificate')), attrs={'class': 'form-control select2'}),
'cn': TextInput(attrs={'class': 'form-control'}),
class Meta(X509CertificateForm.Meta):
model = X509Certificate
fields = X509CertificateForm.Meta.fields + ('cert', 'key', 'chain', 'crl', 'crl_uri')
widgets = X509CertificateForm.Meta.widgets | {
'cert': Textarea(attrs={'class': 'form-control'}),
'key': Textarea(attrs={'class': 'form-control'}),
'chain': Textarea(attrs={'class': 'form-control'}),
Expand Down
6 changes: 6 additions & 0 deletions vulture_os/system/pki/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@
('required', 'Required')
)

CERTIFICATE_TYPE_CHOICES = (
('internal', "Self-Signed Vulture Certificate"),
('letsencrypt', "Let's Encrypt Certificate"),
('external', "External certificate")
)
Comment on lines +146 to +150
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using enumeration types


CERT_PATH = path_join(settings.DBS_PATH, "pki")
ACME_PATH = path_join(settings.DBS_PATH, "acme")
CERT_OWNER = "vlt-os:haproxy"
Expand Down
45 changes: 22 additions & 23 deletions vulture_os/system/pki/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from gui.forms.form_utils import DivErrorList
from services.frontend.models import Frontend
from system.exceptions import VultureSystemConfigError
from system.pki.form import TLSProfileForm, X509ExternalCertificateForm, X509InternalCertificateForm
from system.pki.form import TLSProfileForm, X509CertificateForm, X509ExternalCertificateForm
from system.pki.models import CIPHER_SUITES, PROTOCOLS_HANDLER, TLSProfile, X509Certificate
from system.cluster.models import Cluster
from toolkit.api.responses import build_response, build_form_errors
Expand Down Expand Up @@ -129,36 +129,35 @@ def pki_edit(request, object_id=None, api=False):
if api:
# Don't allow setting internal or letsencrypt certs through APIs
request.JSON['type'] = "external"
request.JSON["is_external"] = True
request.JSON['is_external'] = True
cert_type = request.JSON.get("type")
form = X509ExternalCertificateForm(request.JSON or None, instance=x509_model, error_class=DivErrorList)
else:
cert_type = request.POST.get("type")
form = X509ExternalCertificateForm(request.POST or None, instance=x509_model, error_class=DivErrorList)

""" Internal Vulture Certificate """
if request.method in ("POST", "PUT") and cert_type in ("internal", "letsencrypt"):
form = X509InternalCertificateForm(request.POST or None, instance=x509_model, error_class=DivErrorList)
if form.is_valid():
cn = form.cleaned_data['cn']
name = form.cleaned_data['name']
if cert_type in ("internal", "letsencrypt") or (x509_model and not x509_model.is_external):
form = X509CertificateForm(request.POST or None, instance=x509_model, error_class=DivErrorList)
else:
form = X509ExternalCertificateForm(request.POST or None, instance=x509_model, error_class=DivErrorList)

if form.cleaned_data.get("type") == "letsencrypt":
X509Certificate().gen_letsencrypt(cn, name)
elif form.cleaned_data.get("type") == "internal":
X509Certificate(name=name, cn=cn).gen_cert()
if request.method in ("POST", "PUT") and form.is_valid():
if cert_type == "external":
""" External certificate """
pki = form.save(commit=False)
pki.is_external = True
pki.is_vulture_ca = False
pki.save()

return HttpResponseRedirect('/system/pki/')
else:
form = X509InternalCertificateForm(request.POST or None, instance=x509_model, error_class=DivErrorList)

elif request.method in ("POST", "PUT") and form.is_valid() and cert_type == "external":
""" Internal Vulture or LetsEncrypt Certificate """
cn = form.cleaned_data['cn']
name = form.cleaned_data['name']

""" External certificate """
pki = form.save(commit=False)
pki.is_external = True
pki.is_vulture_ca = False
pki.save()
if not x509_model:
if cert_type == "letsencrypt":
X509Certificate().gen_letsencrypt(cn, name)
elif cert_type == "internal":
X509Certificate(name=name, cn=cn).gen_cert()
pki = form.save()

""" Reload HAProxy on certificate change """
if X509Certificate.objects.filter(certificate_of__listener__isnull=False, id=pki.id).exists() or X509Certificate.objects.filter(certificate_of__server__isnull=False, id=pki.id).exists():
Expand Down
109 changes: 53 additions & 56 deletions vulture_os/system/templates/system/pki_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,65 +26,62 @@ <h1 class="panel-title"><i class="fa fa-sitemap">&nbsp;</i>{% translate "X509 Ce
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="col-sm-4 control-label">{% translate "Certificate type" %}</label>
<div class="col-sm-5">
<div class="col-sm-5">
{{form.type}}
{{form.type.errors|safe}}
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% translate "Certificate type" %}</label>
<div class="col-sm-5">
{{form.type}}
{{form.type.errors|safe}}
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% translate "Friendly name" %}</label>
<div class="col-sm-5">
{{form.name}}
{{form.name.errors|safe}}
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% translate "Friendly name" %}</label>
<div class="col-sm-5">
{{form.name}}
{{form.name.errors|safe}}
</div>
<div class="form-group internal">
<label class="col-sm-4 control-label">{% translate "Common name" %}</label>
<div class="col-sm-5">
{{form.cn}}
{{form.cn.errors|safe}}
</div>
</div>
<div class="form-group internal">
<label class="col-sm-4 control-label">{% translate "Common name" %}</label>
<div class="col-sm-5">
{{form.cn}}
{{form.cn.errors|safe}}
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Certificate" %}</label>
<div class="col-sm-5">
{{form.cert}}
{{form.cert.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Private Key" %}</label>
<div class="col-sm-5">
{{form.key}}
{{form.key.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Certificate Chain" %}</label>
<div class="col-sm-5">
{{form.chain}}
{{form.chain.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM CRL (optional)" %}</label>
<div class="col-sm-5">
{{form.crl}}
{{form.crl.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "URI to fetch CRL (optional)" %}</label>
<div class="col-sm-5">
{{form.crl_uri}}
{{form.crl_uri.errors|safe}}
</div>
</div>

</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Certificate" %}</label>
<div class="col-sm-5">
{{form.cert}}
{{form.cert.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Private Key" %}</label>
<div class="col-sm-5">
{{form.key}}
{{form.key.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM Certificate Chain" %}</label>
<div class="col-sm-5">
{{form.chain}}
{{form.chain.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "PEM CRL (optional)" %}</label>
<div class="col-sm-5">
{{form.crl}}
{{form.crl.errors|safe}}
</div>
</div>
<div class="form-group external">
<label class="col-sm-4 control-label">{% translate "URI to fetch CRL (optional)" %}</label>
<div class="col-sm-5">
{{form.crl_uri}}
{{form.crl_uri.errors|safe}}
</div>
</div>
</div>
</div>
</div>
Expand Down