self signed certificates

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-04 20:00:59 +01:00
parent a7a5f7aacc
commit 3855e6d39b
6 changed files with 255 additions and 15 deletions

View File

@@ -12,17 +12,18 @@ const (
// Domain is gorm data model
type Domain struct {
ID uuid.UUID `gorm:"primary_key;not null;unique;type:uuid;"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
CompanyID *uuid.UUID `gorm:"index;type:uuid;"`
ID uuid.UUID `gorm:"primary_key;not null;unique;type:uuid;"`
CreatedAt *time.Time `gorm:"not null;index;"`
UpdatedAt *time.Time `gorm:"not null;index;"`
CompanyID *uuid.UUID `gorm:"index;type:uuid;"`
ProxyID *uuid.UUID `gorm:"index;type:uuid;"`
Name string `gorm:"not null;unique;"`
Type string `gorm:"not null;default:'regular';"`
Name string `gorm:"not null;unique;"`
Type string `gorm:"not null;default:'regular';"`
ProxyTargetDomain string
ManagedTLSCerts bool `gorm:"not null;index;default:false"`
OwnManagedTLS bool `gorm:"not null;index;default:false"`
SelfSignedTLS bool `gorm:"not null;index;default:false"`
HostWebsite bool `gorm:"not null;"`
PageContent string
PageNotFoundContent string

View File

@@ -22,6 +22,7 @@ type Domain struct {
HostWebsite nullable.Nullable[bool] `json:"hostWebsite"`
ManagedTLS nullable.Nullable[bool] `json:"managedTLS"`
OwnManagedTLS nullable.Nullable[bool] `json:"ownManagedTLS"`
SelfSignedTLS nullable.Nullable[bool] `json:"selfSignedTLS"`
// private key
OwnManagedTLSKey nullable.Nullable[string] `json:"ownManagedTLSKey"`
// cert
@@ -84,10 +85,28 @@ func (d *Domain) Validate() error {
ownManagedTLS, err := d.OwnManagedTLS.Get()
ownManagedTLSSet := err == nil && ownManagedTLS
// cant both have managed and own managed tls
if managedTLS, err := d.ManagedTLS.Get(); err == nil && managedTLS && ownManagedTLSSet {
selfSignedTLS, err := d.SelfSignedTLS.Get()
selfSignedTLSSet := err == nil && selfSignedTLS
managedTLS, err := d.ManagedTLS.Get()
managedTLSSet := err == nil && managedTLS
// count how many TLS options are enabled
tlsCount := 0
if managedTLSSet {
tlsCount++
}
if ownManagedTLSSet {
tlsCount++
}
if selfSignedTLSSet {
tlsCount++
}
// only one TLS option can be enabled at a time
if tlsCount > 1 {
return errs.NewValidationError(errors.New(
"Domain TLS can not both be managed and own managed",
"Domain TLS can only have one option enabled: managed, own managed, or self-signed",
))
}
if ownManagedTLS {
@@ -189,6 +208,14 @@ func (d *Domain) ToDBMap() map[string]any {
m["own_managed_tls"] = d.OwnManagedTLS.MustGet()
}
}
if d.SelfSignedTLS.IsSpecified() {
m["self_signed_tls"] = false
if d.SelfSignedTLS.IsNull() {
m["self_signed_tls"] = nil
} else {
m["self_signed_tls"] = d.SelfSignedTLS.MustGet()
}
}
if d.ProxyID.IsSpecified() {
if d.ProxyID.IsNull() {
m["proxy_id"] = nil
@@ -210,6 +237,7 @@ type DomainOverview struct {
HostWebsite bool `json:"hostWebsite"`
ManagedTLS bool `json:"managedTLS"`
OwnManagedTLS bool `json:"ownManagedTLS"`
SelfSignedTLS bool `json:"selfSignedTLS"`
RedirectURL string `json:"redirectURL"`
CompanyID *uuid.UUID `json:"companyID"`
ProxyID *uuid.UUID `json:"proxyID"`

View File

@@ -285,6 +285,7 @@ func ToDomain(row *database.Domain) *model.Domain {
managedTLS := nullable.NewNullableWithValue(row.ManagedTLSCerts)
ownManagedTLS := nullable.NewNullableWithValue(row.OwnManagedTLS)
selfSignedTLS := nullable.NewNullableWithValue(row.SelfSignedTLS)
hostWebsite := nullable.NewNullableWithValue(row.HostWebsite)
staticPage := nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(row.PageContent))
staticNotFound := nullable.NewNullableWithValue(*vo.NewOptionalString1MBMust(row.PageNotFoundContent))
@@ -299,6 +300,7 @@ func ToDomain(row *database.Domain) *model.Domain {
ProxyTargetDomain: proxyTargetDomain,
ManagedTLS: managedTLS,
OwnManagedTLS: ownManagedTLS,
SelfSignedTLS: selfSignedTLS,
HostWebsite: hostWebsite,
PageContent: staticPage,
PageNotFoundContent: staticNotFound,
@@ -351,6 +353,7 @@ func ToDomainSubset(dbDomain *database.Domain) *model.DomainOverview {
HostWebsite: dbDomain.HostWebsite,
ManagedTLS: dbDomain.ManagedTLSCerts,
OwnManagedTLS: dbDomain.OwnManagedTLS,
SelfSignedTLS: dbDomain.SelfSignedTLS,
RedirectURL: dbDomain.RedirectURL,
CompanyID: dbDomain.CompanyID,
ProxyID: dbDomain.ProxyID,

View File

@@ -17,6 +17,7 @@ import (
"github.com/caddyserver/certmagic"
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/acme"
"github.com/phishingclub/phishingclub/build"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/errs"
@@ -156,6 +157,10 @@ func (d *Domain) createDomain(
if err != nil {
return nil, errs.Wrap(err)
}
domain, err = d.handleSelfSignedTLS(ctx, domain)
if err != nil {
return nil, errs.Wrap(err)
}
// create domain
createdDomainID, err := d.DomainRepository.Insert(
ctx,
@@ -579,6 +584,12 @@ func (d *Domain) updateDomain(
current.OwnManagedTLS.Set(v)
ownManagedTLSIsSet = v
}
wasSelfSignedTLS := current.SelfSignedTLS.MustGet()
selfSignedTLSIsSet := false
if v, err := incoming.SelfSignedTLS.Get(); err == nil {
current.SelfSignedTLS.Set(v)
selfSignedTLSIsSet = v
}
ownManagedTLSKeyIsSet := false
if v, err := incoming.OwnManagedTLSKey.Get(); err == nil {
current.OwnManagedTLSKey.Set(v)
@@ -629,6 +640,13 @@ func (d *Domain) updateDomain(
d.Logger.Warnf("failed to remove own managed TLS", "error", err)
}
}
// if previously was self-signed but not anymore, remove the certs and cache
if wasSelfSignedTLS && !selfSignedTLSIsSet {
err = d.removeSelfSignedTLS(current)
if err != nil {
d.Logger.Warnf("failed to remove self-signed TLS", "error", err)
}
}
// if previously own managed TLS and now is own managed
if !wasOwnManagedTLS && ownManagedTLSIsSet {
if ownManagedTLSKeyIsSet && ownManagedTLSPemIsSet {
@@ -652,6 +670,13 @@ func (d *Domain) updateDomain(
}
}
}
// if previously not self-signed and now is self-signed
if !wasSelfSignedTLS && selfSignedTLSIsSet {
current, err = d.handleSelfSignedTLS(ctx, current)
if err != nil {
return fmt.Errorf("failed to handle self-signed TLS: %s", err)
}
}
// when updating, the own managed tls can previous be set with uploaded
// key and cert, so only if all of them are provided, we handle them
// update domain
@@ -754,6 +779,20 @@ func (d *Domain) deleteDomain(
if domain.ManagedTLS.MustGet() {
d.removeManagedDomainTLS(ctx, domain.Name.MustGet().String())
}
// clean up if TLS was own managed
if domain.OwnManagedTLS.MustGet() {
err = d.removeOwnManagedTLS(domain)
if err != nil {
d.Logger.Warnf("failed to remove own managed TLS during deletion", "error", err)
}
}
// clean up if TLS was self-signed
if domain.SelfSignedTLS.MustGet() {
err = d.removeSelfSignedTLS(domain)
if err != nil {
d.Logger.Warnf("failed to remove self-signed TLS during deletion", "error", err)
}
}
d.AuditLogAuthorized(ae)
return nil
}
@@ -931,6 +970,132 @@ func (d *Domain) removeOwnManagedTLS(
return nil
}
func (d *Domain) handleSelfSignedTLS(
ctx context.Context,
domain *model.Domain) (*model.Domain, error) {
selfSignedTLS, err := domain.SelfSignedTLS.Get()
if err != nil || !selfSignedTLS {
return domain, nil
}
name := domain.Name.MustGet().String()
// create root filesystem for secure certificate operations
root, err := os.OpenRoot(d.OwnManagedCertificatePath)
if err != nil {
return nil, fmt.Errorf("failed to open certificate path: %s", err)
}
defer root.Close()
// validate domain name directory access
_, err = root.Stat(name)
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("invalid domain name for certificate path: %s", err)
}
// build path for certificate operations
certDir := filepath.Join(d.OwnManagedCertificatePath, name)
certKeyPath := filepath.Join(certDir, "cert.key")
certPemPath := filepath.Join(certDir, "cert.pem")
// generate self-signed certificate
info := acme.NewInformationWithDefault()
err = acme.CreateSelfSignedCert(
d.Logger,
info,
[]string{name},
certPemPath,
certKeyPath,
)
if err != nil {
d.Logger.Errorw(
"failed to generate self-signed certificate",
"domain", name,
"error", err,
)
return nil, errs.Wrap(err)
}
// read generated certificate files for caching
keyBytes, err := os.ReadFile(certKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read generated key file: %s", err)
}
pemBytes, err := os.ReadFile(certPemPath)
if err != nil {
return nil, fmt.Errorf("failed to read generated cert file: %s", err)
}
// cache in certmagic
hash, err := d.CertMagicConfig.CacheUnmanagedCertificatePEMBytes(
ctx,
pemBytes,
keyBytes,
[]string{name},
)
if err != nil {
d.Logger.Errorw(
"failed to cache self-signed cert for", name,
"error", err,
)
return nil, errs.Wrap(err)
}
d.Logger.Debugw("cached self-signed TLS",
"domain", name,
"hash", hash,
)
domain.SelfSignedTLS = nullable.NewNullableWithValue(true)
domain.ManagedTLS = nullable.NewNullableWithValue(false)
domain.OwnManagedTLS = nullable.NewNullableWithValue(false)
return domain, nil
}
func (d *Domain) removeSelfSignedTLS(
domain *model.Domain,
) error {
name := domain.Name.MustGet().String()
// create root filesystem for secure certificate operations
root, err := os.OpenRoot(d.OwnManagedCertificatePath)
if err != nil {
return fmt.Errorf("failed to open certificate path for '%s': %s", name, err)
}
defer root.Close()
// validate domain name directory exists
_, err = root.Stat(name)
if err != nil {
if os.IsNotExist(err) {
// directory doesn't exist, nothing to delete
return nil
}
return fmt.Errorf("failed to access certificate directory for '%s': %s", name, err)
}
// build safe path for deletion (validated by OpenRoot)
path := filepath.Join(d.OwnManagedCertificatePath, name)
err = d.FileService.DeleteAll(path)
if err != nil {
return fmt.Errorf("failed to delete self-signed TLS for '%s' as: %s", name, err)
}
d.Logger.Debugw("removed storage for self-signed TLS", "name", name)
// remove from certmagic cache
certs := d.CertMagicCache.AllMatchingCertificates(name)
for _, cert := range certs {
d.CertMagicCache.Remove([]string{cert.Hash()})
d.Logger.Debugw("removed cached TLS",
"domain", name,
"hash", cert.Hash(),
)
}
return nil
}
// validateProxyDomain validates proxy domain configuration
func (d *Domain) validateProxyDomain(ctx context.Context, domain *model.Domain) error {
// validate proxy target domain format

View File

@@ -1198,6 +1198,7 @@ export class API {
* @param {string} domain.proxyTargetDomain
* @param {boolean} domain.managedTLS
* @param {boolean} domain.ownManagedTLS
* @param {boolean} domain.selfSignedTLS
* @param {string} domain.ownManagedTLSKey
* @param {string} domain.ownManagedTLSPem
* @param {boolean} domain.hostWebsite
@@ -1213,6 +1214,7 @@ export class API {
proxyTargetDomain,
managedTLS,
ownManagedTLS,
selfSignedTLS,
ownManagedTLSKey,
ownManagedTLSPem,
hostWebsite,
@@ -1227,6 +1229,7 @@ export class API {
proxyTargetDomain: proxyTargetDomain,
managedTLS: managedTLS,
ownManagedTLS: ownManagedTLS,
selfSignedTLS: selfSignedTLS,
ownManagedTLSKey: ownManagedTLSKey,
ownManagedTLSPem: ownManagedTLSPem,
hostWebsite: hostWebsite,
@@ -1246,6 +1249,7 @@ export class API {
* @param {string} [domain.proxyTargetDomain]
* @param {boolean} domain.managedTLS
* @param {boolean} domain.ownManagedTLS
* @param {boolean} domain.selfSignedTLS
* @param {string} domain.ownManagedTLSKey
* @param {string} domain.ownManagedTLSPem
* @param {boolean} [domain.hostWebsite]
@@ -1261,6 +1265,7 @@ export class API {
proxyTargetDomain,
managedTLS,
ownManagedTLS,
selfSignedTLS,
ownManagedTLSKey,
ownManagedTLSPem,
hostWebsite,
@@ -1272,6 +1277,7 @@ export class API {
const payload = {
managedTLS: managedTLS,
ownManagedTLS: ownManagedTLS,
selfSignedTLS: selfSignedTLS,
ownManagedTLSKey: ownManagedTLSKey,
ownManagedTLSPem: ownManagedTLSPem,
companyID: companyID

View File

@@ -50,6 +50,7 @@
name: null,
managedTLS: true, // managed TLS
ownManagedTLS: false, // custom certificates
selfSignedTLS: false, // self-signed certificates
ownManagedTLSKey: null,
ownManagedTLSPem: null,
hostWebsite: true,
@@ -102,6 +103,23 @@
modalText = '';
modalText = getModalText('domain', modalMode);
}
// handle mutual exclusivity of TLS options
$: if (formValues.managedTLS) {
formValues.ownManagedTLS = false;
formValues.selfSignedTLS = false;
}
$: if (formValues.selfSignedTLS) {
formValues.managedTLS = false;
formValues.ownManagedTLS = false;
}
$: if (formValues.ownManagedTLS) {
formValues.managedTLS = false;
formValues.selfSignedTLS = false;
}
// hooks
onMount(() => {
const context = appStateService.getContext();
@@ -192,6 +210,7 @@
id: formValues.id,
managedTLS: formValues.managedTLS,
ownManagedTLS: formValues.ownManagedTLS,
selfSignedTLS: formValues.selfSignedTLS,
ownManagedTLSKey: formValues.ownManagedTLSKey,
ownManagedTLSPem: formValues.ownManagedTLSPem,
companyID: contextCompanyID
@@ -200,10 +219,11 @@
// for regular domains, send all fields
updateData = {
id: formValues.id,
type: 'regular',
proxyTargetDomain: '',
type: isProxyDomain ? 'proxy' : 'regular',
proxyTargetDomain: formValues.proxyTargetDomain || '',
managedTLS: formValues.managedTLS,
ownManagedTLS: formValues.ownManagedTLS,
selfSignedTLS: formValues.selfSignedTLS,
ownManagedTLSKey: formValues.ownManagedTLSKey,
ownManagedTLSPem: formValues.ownManagedTLSPem,
hostWebsite: formValues.hostWebsite,
@@ -289,6 +309,7 @@
proxyTargetDomain: '',
managedTLS: formValues.managedTLS,
ownManagedTLS: formValues.ownManagedTLS,
selfSignedTLS: formValues.selfSignedTLS,
ownManagedTLSKey: formValues.ownManagedTLSKey,
ownManagedTLSPem: formValues.ownManagedTLSPem,
hostWebsite: formValues.hostWebsite,
@@ -328,6 +349,7 @@
id: formValues.id,
managedTLS: formValues.managedTLS,
ownManagedTLS: formValues.ownManagedTLS,
selfSignedTLS: formValues.selfSignedTLS,
ownManagedTLSKey: formValues.ownManagedTLSKey,
ownManagedTLSPem: formValues.ownManagedTLSPem,
companyID: contextCompanyID
@@ -340,6 +362,7 @@
proxyTargetDomain: '',
managedTLS: formValues.managedTLS,
ownManagedTLS: formValues.ownManagedTLS,
selfSignedTLS: formValues.selfSignedTLS,
ownManagedTLSKey: formValues.ownManagedTLSKey,
ownManagedTLSPem: formValues.ownManagedTLSPem,
hostWebsite: formValues.hostWebsite,
@@ -420,13 +443,14 @@
name: domain.name,
managedTLS: domain.managedTLS,
ownManagedTLS: domain.ownManagedTLS,
ownManagedTLSKey: null,
ownManagedTLSPem: null,
selfSignedTLS: domain.selfSignedTLS,
ownManagedTLSKey: domain.ownManagedTLSKey,
ownManagedTLSPem: domain.ownManagedTLSPem,
hostWebsite: domain.hostWebsite,
pageContent: domain.pageContent,
pageNotFoundContent: domain.pageNotFoundContent,
redirectURL: domain.redirectURL,
staticContent: domain.staticContent
staticContent: domain.staticContent || ''
};
// Store domain object for proxy info display
@@ -497,6 +521,7 @@
name: domain.name,
managedTLS: domain.managedTLS,
ownManagedTLS: domain.ownManagedTLS,
selfSignedTLS: domain.selfSignedTLS,
ownManagedTLSKey: null,
ownManagedTLSPem: null,
hostWebsite: domain.hostWebsite,
@@ -528,6 +553,7 @@
name: null,
managedTLS: true, // managed TLS
ownManagedTLS: false, // custom certificates
selfSignedTLS: false, // self-signed certificates
ownManagedTLSKey: null,
ownManagedTLSPem: null,
hostWebsite: true,
@@ -565,6 +591,7 @@
name: domain.name,
managedTLS: domain.managedTLS,
ownManagedTLS: domain.ownManagedTLS,
selfSignedTLS: domain.selfSignedTLS,
ownManagedTLSKey: null,
ownManagedTLSPem: null,
hostWebsite: domain.hostWebsite,
@@ -660,7 +687,7 @@
<TableCellCheck value={domain.hostWebsite} />
<TableCellCheck value={!!domain.redirectURL} />
<TableCellCheck value={domain.managedTLS} />
<TableCellCheck value={domain.ownManagedTLS} />
<TableCellCheck value={domain.ownManagedTLS || domain.selfSignedTLS} />
<TableCell>
<div class="flex justify-center">
<span title={domain.type === 'proxy' ? 'Proxy Domain' : 'Regular Domain'}>
@@ -796,6 +823,16 @@
toolTipText="Managed TLS via. public certificate authority"
/>
<SelectSquare
label="Self-Signed Certificates"
options={[
{ value: true, label: 'Enable' },
{ value: false, label: 'Disable' }
]}
bind:value={formValues.selfSignedTLS}
toolTipText="Automatically generate self-signed certificates for TLS"
/>
<SelectSquare
label="Custom Certificates"
options={[