Pārlūkot izejas kodu

chore: merge pull request #3

Northon Torga 1 gadu atpakaļ
vecāks
revīzija
6fa511da08
33 mainītis faili ar 1838 papildinājumiem un 4 dzēšanām
  1. 24 0
      src/domain/dto/addSslPair.go
  2. 75 0
      src/domain/entity/sslCertificate.go
  3. 27 0
      src/domain/entity/sslPair.go
  4. 11 0
      src/domain/repository/sslCmdRepo.go
  5. 11 0
      src/domain/repository/sslQueryRepo.go
  6. 28 0
      src/domain/useCase/addSslPair.go
  7. 35 0
      src/domain/useCase/deleteSslPair.go
  8. 12 0
      src/domain/useCase/getSslPairs.go
  9. 39 0
      src/domain/valueObject/helper/interfaceToUint.go
  10. 40 0
      src/domain/valueObject/sslCertificateContent.go
  11. 85 0
      src/domain/valueObject/sslCertificateContent_test.go
  12. 83 0
      src/domain/valueObject/sslId.go
  13. 39 0
      src/domain/valueObject/sslId_test.go
  14. 39 0
      src/domain/valueObject/sslPrivateKey.go
  15. 74 0
      src/domain/valueObject/sslPrivateKey_test.go
  16. 35 0
      src/domain/valueObject/unixFilePath.go
  17. 20 0
      src/domain/valueObject/unixFilePath_test.go
  18. 24 0
      src/infra/helper/getFileContent.go
  19. 22 0
      src/infra/helper/getRegexFirstGroup.go
  20. 21 0
      src/infra/helper/makeDir.go
  21. 2 2
      src/infra/o11y/getOverview.go
  22. 1 1
      src/infra/services/assets/httpd_config.conf
  23. 12 1
      src/infra/services/install.go
  24. 136 0
      src/infra/sslCmdRepo.go
  25. 233 0
      src/infra/sslQueryRepo.go
  26. 99 0
      src/presentation/api/controller/sslController.go
  27. 179 0
      src/presentation/api/docs/docs.go
  28. 179 0
      src/presentation/api/docs/swagger.json
  29. 113 0
      src/presentation/api/docs/swagger.yaml
  30. 8 0
      src/presentation/api/router.go
  31. 112 0
      src/presentation/cli/controller/sslController.go
  32. 13 0
      src/presentation/cli/router.go
  33. 7 0
      src/presentation/shared/checkEnvs.go

+ 24 - 0
src/domain/dto/addSslPair.go

@@ -0,0 +1,24 @@
+package dto
+
+import (
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/valueObject"
+)
+
+type AddSslPair struct {
+	Hostname    valueObject.Fqdn          `json:"hostname"`
+	Certificate entity.SslCertificate     `json:"certificate"`
+	Key         valueObject.SslPrivateKey `json:"key"`
+}
+
+func NewAddSslPair(
+	hostname valueObject.Fqdn,
+	certificate entity.SslCertificate,
+	key valueObject.SslPrivateKey,
+) AddSslPair {
+	return AddSslPair{
+		Hostname:    hostname,
+		Certificate: certificate,
+		Key:         key,
+	}
+}

+ 75 - 0
src/domain/entity/sslCertificate.go

@@ -0,0 +1,75 @@
+package entity
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+
+	"github.com/speedianet/sam/src/domain/valueObject"
+)
+
+type SslCertificate struct {
+	Id          valueObject.SslId
+	Certificate valueObject.SslCertificateContent
+	CommonName  *valueObject.Fqdn
+	IssuedAt    valueObject.UnixTime
+	ExpiresAt   valueObject.UnixTime
+	IsCA        bool
+}
+
+func NewSslCertificate(
+	sslCertificateContent valueObject.SslCertificateContent,
+) (SslCertificate, error) {
+	block, _ := pem.Decode([]byte(sslCertificateContent.String()))
+	if block == nil {
+		return SslCertificate{}, errors.New("SslCertificateContentDecodeError")
+	}
+
+	parsedCert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return SslCertificate{}, errors.New("SslCertificateContentParseError")
+	}
+
+	sslCertificateId, err := valueObject.NewSslIdFromSslCertificateContent(
+		sslCertificateContent,
+	)
+	if err != nil {
+		return SslCertificate{}, err
+	}
+
+	issuedAt := valueObject.UnixTime(parsedCert.NotBefore.Unix())
+	expiresAt := valueObject.UnixTime(parsedCert.NotAfter.Unix())
+
+	var commonNamePtr *valueObject.Fqdn
+	commonNamePtr = nil
+	if !parsedCert.IsCA {
+		commonName, err := valueObject.NewFqdn(parsedCert.Subject.CommonName)
+		if err != nil {
+			return SslCertificate{}, errors.New("InvalidSslCertificateCommonName")
+		}
+		commonNamePtr = &commonName
+	}
+
+	return SslCertificate{
+		Id:          sslCertificateId,
+		Certificate: sslCertificateContent,
+		CommonName:  commonNamePtr,
+		IssuedAt:    issuedAt,
+		ExpiresAt:   expiresAt,
+		IsCA:        parsedCert.IsCA,
+	}, nil
+}
+
+func NewSslCertificatePanic(
+	sslCertificateContent valueObject.SslCertificateContent,
+) SslCertificate {
+	sslCertificate, err := NewSslCertificate(sslCertificateContent)
+	if err != nil {
+		panic(err)
+	}
+	return sslCertificate
+}
+
+func (sslCertificate SslCertificate) String() string {
+	return sslCertificate.Certificate.String()
+}

+ 27 - 0
src/domain/entity/sslPair.go

@@ -0,0 +1,27 @@
+package entity
+
+import "github.com/speedianet/sam/src/domain/valueObject"
+
+type SslPair struct {
+	Id                valueObject.SslId         `json:"sslPairId"`
+	Hostname          valueObject.Fqdn          `json:"hostname"`
+	Certificate       SslCertificate            `json:"certificate"`
+	Key               valueObject.SslPrivateKey `json:"key"`
+	ChainCertificates []SslCertificate          `json:"chainCertificates"`
+}
+
+func NewSslPair(
+	sslPairId valueObject.SslId,
+	hostname valueObject.Fqdn,
+	certificate SslCertificate,
+	key valueObject.SslPrivateKey,
+	chainCertificates []SslCertificate,
+) SslPair {
+	return SslPair{
+		Id:                sslPairId,
+		Hostname:          hostname,
+		Certificate:       certificate,
+		Key:               key,
+		ChainCertificates: chainCertificates,
+	}
+}

+ 11 - 0
src/domain/repository/sslCmdRepo.go

@@ -0,0 +1,11 @@
+package repository
+
+import (
+	"github.com/speedianet/sam/src/domain/dto"
+	"github.com/speedianet/sam/src/domain/valueObject"
+)
+
+type SslCmdRepo interface {
+	Add(addSslPair dto.AddSslPair) error
+	Delete(sslId valueObject.SslId) error
+}

+ 11 - 0
src/domain/repository/sslQueryRepo.go

@@ -0,0 +1,11 @@
+package repository
+
+import (
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/valueObject"
+)
+
+type SslQueryRepo interface {
+	GetSslPairs() ([]entity.SslPair, error)
+	GetSslPairById(sslId valueObject.SslId) (entity.SslPair, error)
+}

+ 28 - 0
src/domain/useCase/addSslPair.go

@@ -0,0 +1,28 @@
+package useCase
+
+import (
+	"errors"
+	"log"
+
+	"github.com/speedianet/sam/src/domain/dto"
+	"github.com/speedianet/sam/src/domain/repository"
+)
+
+func AddSslPair(
+	sslCmdRepo repository.SslCmdRepo,
+	addSslPair dto.AddSslPair,
+) error {
+	err := sslCmdRepo.Add(addSslPair)
+	if err != nil {
+		log.Printf("AddSslPairError: %s", err)
+		return errors.New("AddSslPairInfraError")
+	}
+
+	log.Printf(
+		"SSL '%v' added in '%v' hostname.",
+		addSslPair.Certificate.Id.String(),
+		addSslPair.Hostname.String(),
+	)
+
+	return nil
+}

+ 35 - 0
src/domain/useCase/deleteSslPair.go

@@ -0,0 +1,35 @@
+package useCase
+
+import (
+	"errors"
+	"log"
+
+	"github.com/speedianet/sam/src/domain/repository"
+	"github.com/speedianet/sam/src/domain/valueObject"
+)
+
+func DeleteSslPair(
+	sslQueryRepo repository.SslQueryRepo,
+	sslCmdRepo repository.SslCmdRepo,
+	sslId valueObject.SslId,
+) error {
+	sslPair, err := sslQueryRepo.GetSslPairById(sslId)
+	if err != nil {
+		log.Printf("SslPairNotFound: %s", err)
+		return errors.New("SslPairNotFound")
+	}
+
+	err = sslCmdRepo.Delete(sslId)
+	if err != nil {
+		log.Printf("DeleteSslPairError: %s", err)
+		return errors.New("DeleteSslPairInfraError")
+	}
+
+	log.Printf(
+		"SSL '%v' of '%v' hostname deleted.",
+		sslId,
+		sslPair.Hostname.String(),
+	)
+
+	return nil
+}

+ 12 - 0
src/domain/useCase/getSslPairs.go

@@ -0,0 +1,12 @@
+package useCase
+
+import (
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/repository"
+)
+
+func GetSslPairs(
+	sslQueryRepo repository.SslQueryRepo,
+) ([]entity.SslPair, error) {
+	return sslQueryRepo.GetSslPairs()
+}

+ 39 - 0
src/domain/valueObject/helper/interfaceToUint.go

@@ -0,0 +1,39 @@
+package voHelper
+
+import (
+	"errors"
+	"reflect"
+	"strconv"
+)
+
+func InterfaceToUint(input interface{}) (uint64, error) {
+	var output uint64
+	var err error
+	var defaultErr error = errors.New("InvalidInput")
+	switch v := input.(type) {
+	case string:
+		output, err = strconv.ParseUint(v, 10, 64)
+	case int, int8, int16, int32, int64:
+		intValue := reflect.ValueOf(v).Int()
+		if intValue < 0 {
+			err = defaultErr
+		}
+		output = uint64(intValue)
+	case uint, uint8, uint16, uint32, uint64:
+		output = uint64(reflect.ValueOf(v).Uint())
+	case float32, float64:
+		floatValue := reflect.ValueOf(v).Float()
+		if floatValue < 0 {
+			err = defaultErr
+		}
+		output = uint64(floatValue)
+	default:
+		err = defaultErr
+	}
+
+	if err != nil {
+		return 0, defaultErr
+	}
+
+	return output, nil
+}

+ 40 - 0
src/domain/valueObject/sslCertificateContent.go

@@ -0,0 +1,40 @@
+package valueObject
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+)
+
+type SslCertificateContent string
+
+func NewSslCertificateContent(sslCertificate string) (SslCertificateContent, error) {
+	certificate := SslCertificateContent(sslCertificate)
+	if !certificate.isValid() {
+		return "", errors.New("InvalidSslCertificateContent")
+	}
+
+	return certificate, nil
+}
+
+func NewSslCertificateContentPanic(certificate string) SslCertificateContent {
+	sslCertificate, err := NewSslCertificateContent(certificate)
+	if err != nil {
+		panic(err)
+	}
+	return sslCertificate
+}
+
+func (sslCertificate SslCertificateContent) isValid() bool {
+	block, _ := pem.Decode([]byte(sslCertificate))
+	if block == nil {
+		return false
+	}
+
+	_, err := x509.ParseCertificate(block.Bytes)
+	return err == nil
+}
+
+func (sslCertificate SslCertificateContent) String() string {
+	return string(sslCertificate)
+}

+ 85 - 0
src/domain/valueObject/sslCertificateContent_test.go

@@ -0,0 +1,85 @@
+package valueObject
+
+import "testing"
+
+func TestSslCertificateContent(t *testing.T) {
+	t.Run("ValidSslCertificateContent", func(t *testing.T) {
+		validSslCert := `
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
+cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
+WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
+TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
+bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q
+5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC
+7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa
+BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF
+BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy
+LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz
+cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf
+BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG
+AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t
+L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+
+gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283
+TZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq
+0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW
+RvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh
+yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==
+-----END CERTIFICATE-----`
+
+		_, err := NewSslCertificateContent(validSslCert)
+		if err != nil {
+			t.Errorf("Expected no error for dummy SSL certificate, got %v", err)
+		}
+	})
+
+	t.Run("InvalidSslCertificateContent", func(t *testing.T) {
+		invalidSslCerts := []string{
+			`-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
+cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
+WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
+TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
+bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q
+5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC
+7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa
+BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF
+BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy
+LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz
+cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf
+BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG
+AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t
+L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+
+gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283
+-----END CERTIFICATE-----`,
+			`MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
+cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
+WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
+TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
+bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q
+5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC
+7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa
+BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF
+BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy
+LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz
+cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf
+BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG
+AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t
+L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+
+gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283
+TZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq
+0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW
+RvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh
+yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==`,
+		}
+		for sslCertIndex, sslCert := range invalidSslCerts {
+			_, err := NewSslCertificateContent(sslCert)
+			if err == nil {
+				t.Errorf("Expected error for '%v' SSL certificate, got nil", sslCertIndex)
+			}
+		}
+	})
+}

+ 83 - 0
src/domain/valueObject/sslId.go

@@ -0,0 +1,83 @@
+package valueObject
+
+import (
+	"encoding/hex"
+	"errors"
+	"regexp"
+
+	"golang.org/x/crypto/sha3"
+)
+
+const sslIdExpression = "^[a-fA-F0-9]{64}$"
+
+type SslId string
+
+func NewSslId(value string) (SslId, error) {
+	sslId := SslId(value)
+	if !sslId.isValid() {
+		return "", errors.New("InvalidSslId")
+	}
+
+	return sslId, nil
+}
+
+func NewSslIdPanic(value string) SslId {
+	sslId, err := NewSslId(value)
+	if err != nil {
+		panic(err)
+	}
+
+	return sslId
+}
+
+func (sslId SslId) isValid() bool {
+	sslIdRegex := regexp.MustCompile(sslIdExpression)
+	return sslIdRegex.MatchString(string(sslId))
+}
+
+func sslIdFactory(value string) (SslId, error) {
+	hash := sha3.New256()
+	_, err := hash.Write([]byte(value))
+	if err != nil {
+		return "", errors.New("InvalidSslId")
+	}
+	sslIdBytes := hash.Sum(nil)
+	sslIdStr := hex.EncodeToString(sslIdBytes)
+
+	return NewSslId(sslIdStr)
+}
+
+func NewSslIdFromSslPairContent(
+	sslCertificate SslCertificateContent,
+	sslChainCertificates []SslCertificateContent,
+	sslPrivateKey SslPrivateKey,
+) (SslId, error) {
+	var sslChainCertificatesMerged string
+	for _, sslChainCertificate := range sslChainCertificates {
+		sslChainCertificatesMerged += sslChainCertificate.String() + "\n"
+	}
+
+	contentToEncode := sslCertificate.String() + "\n" + sslChainCertificatesMerged + "\n" + sslPrivateKey.String()
+
+	sslId, err := sslIdFactory(contentToEncode)
+	if err != nil {
+		return "", errors.New("InvalidSslIdFromSslPairContent")
+	}
+
+	return sslId, nil
+}
+
+func NewSslIdFromSslCertificateContent(
+	sslCertificate SslCertificateContent,
+) (SslId, error) {
+	sslId, err := sslIdFactory(sslCertificate.String())
+	if err != nil {
+		return "", errors.New("InvalidSslIdFromSslCertificateContent")
+	}
+
+	return sslId, nil
+}
+
+func (sslId SslId) String() string {
+	return string(sslId)
+}

+ 39 - 0
src/domain/valueObject/sslId_test.go

@@ -0,0 +1,39 @@
+package valueObject
+
+import (
+	"testing"
+)
+
+func TestNewSslId(t *testing.T) {
+	t.Run("ValidHashId", func(t *testing.T) {
+		validSslCert := `
+-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
+cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
+WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
+TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
+bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q
+5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC
+7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa
+BgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF
+BwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy
+LmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz
+cDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf
+BgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG
+AQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t
+L0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+
+gtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283
+TZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq
+0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW
+RvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh
+yE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==
+-----END CERTIFICATE-----`
+
+		SslCertificateContent, _ := NewSslCertificateContent(validSslCert)
+		_, err := NewSslIdFromSslCertificateContent(SslCertificateContent)
+		if err != nil {
+			t.Errorf("Expected no error for %s, got %v", SslCertificateContent.String(), err)
+		}
+	})
+}

+ 39 - 0
src/domain/valueObject/sslPrivateKey.go

@@ -0,0 +1,39 @@
+package valueObject
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+)
+
+type SslPrivateKey string
+
+func NewSslPrivateKey(privateKey string) (SslPrivateKey, error) {
+	sslPrivateKey := SslPrivateKey(privateKey)
+	if !sslPrivateKey.isValid() {
+		return "", errors.New("SslPrivateKeyError")
+	}
+
+	return sslPrivateKey, nil
+}
+
+func NewSslPrivateKeyPanic(privateKey string) SslPrivateKey {
+	sslPrivateKey, err := NewSslPrivateKey(privateKey)
+	if err != nil {
+		panic(err)
+	}
+	return sslPrivateKey
+}
+
+func (sslPrivateKey SslPrivateKey) isValid() bool {
+	block, _ := pem.Decode([]byte(sslPrivateKey))
+	if block == nil {
+		return false
+	}
+	_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+	return err == nil
+}
+
+func (sslPrivateKey SslPrivateKey) String() string {
+	return string(sslPrivateKey)
+}

+ 74 - 0
src/domain/valueObject/sslPrivateKey_test.go

@@ -0,0 +1,74 @@
+package valueObject
+
+import "testing"
+
+func TestSslPrivateKey(t *testing.T) {
+	t.Run("ValidSslPrivateKey", func(t *testing.T) {
+		validSslPrivateKey := `
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAziJ8BEmVq/cSznb7aYRL5YBJjhMZVxC/jqT2Q/LKWFjX38Er
+LHT3khhdlKyrh/7AgfN+Us1Q7/eHq3PKX7Z0lgk+9LssNnaH67bj9lqJcILlToc5
+UrhZLHe2Q3xUlfyveDoheepcbBiqO7xzuUNl53KpT4FlF3DBO94wMNNqOjeNslHv
+lfpc/gJlZ5IBuxGG1+xjA75bGqDnFqyjEyUxrNxJyM70NAL4J+3rScdvXpdMKbMn
+IqC2s0mrK1iuPL4tryOG5/dES5BPJdRIrEAC/G4Kf6h8xD/QI9zzPmiZ4UJqb0A2
+TcmYexc40BnkgHO7XWr1zP20oZvSS46C/v3kAwIDAQABAoIBAQCd02Vk2vpP2jJ6
+BjtkhLiflWO79f+W2+nuy3sKd2BZ2Fwgo4Ps2/mZ0DIGXVZQH8tBNC9qMm1f7gPg
+UB2Ivufw4E9ljdHCOWrEHRnZS2Sj0nTDdWF8Zk1QcLAKZ61T0U6AHPH4qGnvEct1
+RUrNdD8XwIDFsOq30csBjZMULyrMOtC5B39mi46cWiQVjoabiOm7IDZFUFEq9Oy9
+ZGcyUWRce60NVcUgYiH1OTY3JNSdAIWXyeaViGNvIdQFt9XLnWkSCF2OZ+WzANy1
+2GePJdG6Jnn2XYsvpKy2mQXp+ULwejPN/KTcuIKPAL29B/wCOtZAXCiE6yib5g9a
+98YlWcjBAoGBAO2NWG7hOFyveijjJNEq9MyhhALu/o3xM8HwPQJOL1moWt4uySUc
+nbnl14YiuZjpoGbuL5143NXN2sJA+XnTBq2pRAPBJS1OLLkY1W+BVEOAc72TRV9v
+egWOHomgUqyInfRTmutnFCVyaAOURRpiXkPBMRLfN9WemVBBbuOAxO/zAoGBAN4k
+i4FEXqHFKp2OR7iRQenP/mSQGTQcQ1zUeWun02nEfMzosFcyioFZwQi3NMvtdC7u
+meHDuoGwLH+YcoM3/5NKUn1Kccl3x3tIbRy9GhavX2NBQ4v0DX2h7acpN4soWlHs
+v07AUMiYHvxEi+t2R7UptUufiwIwSlCmBQwGGE+xAoGBAOYu4FIQypyFLMoRz8se
+5Laki1aMXv0LjCuQro1dVWR7ThGdJCth3zQTExRW8aDKQTN7+YeNZe+G2UMB0rvJ
+T99W9SDuNyf/aDazaZ3yo8QE5CH+YmpnisV3QP/66iFlACmQGb2g1FS01zUgpxU5
+3D2rJfIzedb1J3os7VZloG8hAoGBANFxJ07DhW2Elf9izGBKJBksj6+E5R5qn2CA
+u9Iys3N/XCNeKBSuhEQcuZFcGp1Czk4JjHB9t/Tag7nxo9XwEDlw04FplQrcsemc
+ibOU32oQAyFzwRnNCoMvDwCSLdo4O6AOVPkM/Z2DP4OdpUZliIpYPqSEUe3IVejf
+/tYtUPKhAoGAQBzkCzCunVBtSuVLPT5/9NVVCj1nfxmXdLq1+0bD5q6m2XiW1G7T
+YvdMz2Th5XVtTfNHhLIKpMyrq7sstb6lsQPKNKSpBuyHa8oooWHrkxf7VVBrN7v8
+en/D01Fd9hVhXyGKaOk/nEDxB8fTgQ1dE9JhxUiqHN4Po1ktNG/P8aU=
+-----END RSA PRIVATE KEY-----`
+
+		_, err := NewSslPrivateKey(validSslPrivateKey)
+		if err != nil {
+			t.Errorf("Expected no error for dummy SSL private key, got %v", err)
+		}
+	})
+
+	t.Run("InvalidSslPrivateKey", func(t *testing.T) {
+		invalidSslPrivateKeys := []string{
+			`-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlRuRnThUjU8/prwYxbty
+WPT9pURI3lbsKMiB6Fn/VHOKE13p4D8xgOCADpdRagdT6n4etr9atzDKUSvpMtR3
+CP5noNc97WiNCggBjVWhs7szEe8ugyqF23XwpHQ6uV1LKH50m92MbOWfCtjU9p/x
+qhNpQQ1AZhqNy5Gevap5k8XzRmjSldNAFZMY7Yv3Gi+nyCwGwpVtBUwhuLzgNFK/
+yDtw2WcWmUU7NuC8Q6MWvPebxVtCfVp/iQU6q60yyt6aGOBkhAX0LpKAEhKidixY
+nP9PNVBvxgu3XZ4P36gZV6+ummKdBVnc3NqwBLu5+CcdRdusmHPHd5pHf4/38Z3/
+6qU2a/fPvWzceVTEgZ47QjFMTCTmCwNt29cvi7zZeQzjtwQgn4ipN9NibRH/Ax/q
+TbIzHfrJ1xa2RteWSdFjwtxi9C20HUkj
+-----END PUBLIC KEY-----`,
+			`MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlRuRnThUjU8/prwYxbty
+WPT9pURI3lbsKMiB6Fn/VHOKE13p4D8xgOCADpdRagdT6n4etr9atzDKUSvpMtR3
+CP5noNc97WiNCggBjVWhs7szEe8ugyqF23XwpHQ6uV1LKH50m92MbOWfCtjU9p/x
+qhNpQQ1AZhqNy5Gevap5k8XzRmjSldNAFZMY7Yv3Gi+nyCwGwpVtBUwhuLzgNFK/
+yDtw2WcWmUU7NuC8Q6MWvPebxVtCfVp/iQU6q60yyt6aGOBkhAX0LpKAEhKidixY
+nP9PNVBvxgu3XZ4P36gZV6+ummKdBVnc3NqwBLu5+CcdRdusmHPHd5pHf4/38Z3/
+6qU2a/fPvWzceVTEgZ47QjFMTCTmCwNt29cvi7zZeQzjtwQgn4ipN9NibRH/Ax/q
+TbIzHfrJ1xa2RteWSdFjwtxi9C20HUkjXSeI4YlzQMH0fPX6KCE7aVePTOnB69I/
+a9/q96DiXZajwlpq3wFctrs1oXqBp5DVrCIj8hU2wNgB7LtQ1mCtsYz//heai0K9
+PhE4X6hiE0YmeAZjR0uHl8M/5aW9xCoJ72+12kKpWAa0SFRWLy6FejNYCYpkupVJ
+yecLk/4L1W0l6jQQZnWErXZYe0PNFcmwGXy1Rep83kfBRNKRy5tvocalLlwXLdUk
+AIU+2GKjyT3iMuzZxxFxPFMCAwEAAQ==`,
+		}
+		for sslPrivateKeyIndex, sslPrivateKey := range invalidSslPrivateKeys {
+			_, err := NewSslPrivateKey(sslPrivateKey)
+			if err == nil {
+				t.Errorf("Expected error for '%v' SSL private key, got nil", sslPrivateKeyIndex)
+			}
+		}
+	})
+}

+ 35 - 0
src/domain/valueObject/unixFilePath.go

@@ -0,0 +1,35 @@
+package valueObject
+
+import (
+	"errors"
+	"regexp"
+)
+
+const unixFilePathRegexExpression = `^\/(?:[\w\p{Latin}\. \-]+\/)*[\w\p{Latin}\. \-]+$`
+
+type UnixFilePath string
+
+func NewUnixFilePath(unixFilePathStr string) (UnixFilePath, error) {
+	unixFilePath := UnixFilePath(unixFilePathStr)
+	if !unixFilePath.isValid() {
+		return "", errors.New("InvalidUnixFilePath")
+	}
+	return unixFilePath, nil
+}
+
+func NewUnixFilePathPanic(unixFilePathStr string) UnixFilePath {
+	unixFilePath, err := NewUnixFilePath(unixFilePathStr)
+	if err != nil {
+		panic(err)
+	}
+	return unixFilePath
+}
+
+func (unixFilePath UnixFilePath) isValid() bool {
+	unixFilePathRegexRegex := regexp.MustCompile(unixFilePathRegexExpression)
+	return unixFilePathRegexRegex.MatchString(string(unixFilePath))
+}
+
+func (unixFilePath UnixFilePath) String() string {
+	return string(unixFilePath)
+}

+ 20 - 0
src/domain/valueObject/unixFilePath_test.go

@@ -0,0 +1,20 @@
+package valueObject
+
+import "testing"
+
+func TestUnixFilePath(t *testing.T) {
+	t.Run("ValidUnixFilePath", func(t *testing.T) {
+		validUnixFilePaths := []string{
+			"/speedia/ssl_crt.pem",
+			"/speedia/ssl_key.pem",
+			"/usr/local/test.sh",
+			"/speedia/ssl_crt",
+		}
+		for _, name := range validUnixFilePaths {
+			_, err := NewUnixFilePath(name)
+			if err != nil {
+				t.Errorf("Expected no error for %s, got %v", name, err)
+			}
+		}
+	})
+}

+ 24 - 0
src/infra/helper/getFileContent.go

@@ -0,0 +1,24 @@
+package infraHelper
+
+import (
+	"errors"
+	"log"
+	"os"
+)
+
+func GetFileContent(filePath string) (string, error) {
+	_, err := os.Stat(filePath)
+	if err != nil {
+		log.Printf("FailedToGetFileContent: %v", err)
+		return "", errors.New("FailedToGetFileContent")
+	}
+
+	fileContentBytes, err := os.ReadFile(filePath)
+	if err != nil {
+		log.Printf("FailedToGetFileContent: %v", err)
+		return "", errors.New("FailedToGetFileContent")
+	}
+	fileContentStr := string(fileContentBytes)
+
+	return fileContentStr, nil
+}

+ 22 - 0
src/infra/helper/getRegexFirstGroup.go

@@ -0,0 +1,22 @@
+package infraHelper
+
+import (
+	"errors"
+	"regexp"
+)
+
+func GetRegexFirstGroup(input string, regexExpression string) (string, error) {
+	regex := regexp.MustCompile(regexExpression)
+	match := regex.FindStringSubmatch(input)
+
+	if len(match) < 2 {
+		return "", errors.New("RegexGroupNotFound")
+	}
+
+	firstGroup := match[1]
+	if len(firstGroup) == 0 {
+		return "", errors.New("EmptyRegexGroup")
+	}
+
+	return firstGroup, nil
+}

+ 21 - 0
src/infra/helper/makeDir.go

@@ -0,0 +1,21 @@
+package infraHelper
+
+import (
+	"os"
+)
+
+func MakeDir(dirPath string) error {
+	_, err := os.Stat(dirPath)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return err
+		}
+	}
+
+	err = os.MkdirAll(dirPath, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 2 - 2
src/infra/o11y/getOverview.go

@@ -34,12 +34,12 @@ func (repo GetOverview) isCgroupV2() bool {
 }
 
 func (repo GetOverview) getFileContent(file string) (string, error) {
-	fileContent, err := os.ReadFile(file)
+	fileContent, err := infraHelper.GetFileContent(file)
 	if err != nil {
 		return "", err
 	}
 
-	return strings.TrimSpace(string(fileContent)), nil
+	return strings.TrimSpace(fileContent), nil
 }
 
 func (repo GetOverview) getCpuQuota() (int64, error) {

+ 1 - 1
src/infra/services/assets/httpd_config.conf

@@ -270,7 +270,7 @@ module cache {
   ls_enabled              1
 }
 
-virtualhost app {
+virtualhost speedia.net {
   vhRoot                  /app/
   configFile              /app/conf/vhconf.conf
   allowSymbolLink         1

+ 12 - 1
src/infra/services/install.go

@@ -152,6 +152,18 @@ func installOls() error {
 		return errors.New("CopyAssetsError")
 	}
 
+	virtualHost := os.Getenv("VIRTUAL_HOST")
+	_, err = infraHelper.RunCmd(
+		"sed",
+		"-i",
+		"s/speedia.net/"+virtualHost+"/g",
+		"/usr/local/lsws/conf/httpd_config.conf",
+	)
+	if err != nil {
+		log.Printf("RenameHttpdVHostError: %s", err)
+		return errors.New("RenameHttpdVHostError")
+	}
+
 	err = copyAssets(
 		"vhconf.conf",
 		"/app/conf/vhconf.conf",
@@ -161,7 +173,6 @@ func installOls() error {
 		return errors.New("CopyAssetsError")
 	}
 
-	virtualHost := os.Getenv("VIRTUAL_HOST")
 	_, err = infraHelper.RunCmd(
 		"sed",
 		"-i",

+ 136 - 0
src/infra/sslCmdRepo.go

@@ -0,0 +1,136 @@
+package infra
+
+import (
+	"errors"
+	"os"
+	"regexp"
+	"strings"
+	"unicode"
+
+	"github.com/speedianet/sam/src/domain/dto"
+	"github.com/speedianet/sam/src/domain/valueObject"
+	infraHelper "github.com/speedianet/sam/src/infra/helper"
+)
+
+type SslCmdRepo struct{}
+
+func (repo SslCmdRepo) vhsslConfigFactory(
+	sslCertFilePath string,
+	sslKeyFilePath string,
+	isChained bool,
+) string {
+	vhsslChainedConfig := ""
+	sslCertChain := "0"
+	if isChained {
+		sslCertChain = "1"
+		vhsslChainedConfig = `
+  CACertPath ` + sslCertFilePath + `
+  CACertFile ` + sslCertFilePath + ``
+	}
+
+	vhsslConfigBreakline := "\n\n"
+	vhsslConfig := `
+vhssl {
+  keyFile    ` + sslKeyFilePath + `
+  certFile   ` + sslCertFilePath + `
+  certChain  ` + sslCertChain +
+		vhsslChainedConfig + `
+}` + vhsslConfigBreakline
+
+	return vhsslConfig
+}
+
+func (repo SslCmdRepo) Add(addSslPair dto.AddSslPair) error {
+	sslQueryRepo := SslQueryRepo{}
+
+	vhostConfigFilePath, err := sslQueryRepo.GetVhostConfigFilePath(addSslPair.Hostname)
+	if err != nil {
+		return err
+	}
+
+	sslBaseDirPath := "/speedia/pki/" + addSslPair.Hostname.String()
+	sslKeyFilePath := sslBaseDirPath + "/ssl.key"
+	sslCertFilePath := sslBaseDirPath + "/ssl.crt"
+
+	err = infraHelper.MakeDir(sslBaseDirPath)
+	if err != nil {
+		return err
+	}
+
+	err = infraHelper.UpdateFile(sslCertFilePath, addSslPair.Certificate.String(), true)
+	if err != nil {
+		return err
+	}
+
+	err = infraHelper.UpdateFile(sslKeyFilePath, addSslPair.Key.String(), true)
+	if err != nil {
+		return err
+	}
+
+	sslPairCertificate := addSslPair.Certificate
+	sslCertificates, err := sslQueryRepo.SslCertificatesFactory(
+		sslPairCertificate.Certificate,
+	)
+	if err != nil {
+		return err
+	}
+
+	newSsl, err := sslQueryRepo.SslPairFactory(
+		addSslPair.Hostname,
+		addSslPair.Key,
+		sslCertificates,
+	)
+	if err != nil {
+		return err
+	}
+
+	isChainedCert := true
+	if len(newSsl.ChainCertificates) == 1 {
+		isChainedCert = false
+	}
+
+	vhsslConfig := repo.vhsslConfigFactory(
+		sslCertFilePath,
+		sslKeyFilePath,
+		isChainedCert,
+	)
+	err = infraHelper.UpdateFile(vhostConfigFilePath.String(), vhsslConfig, false)
+	return err
+}
+
+func (repo SslCmdRepo) Delete(sslId valueObject.SslId) error {
+	sslQueryRepo := SslQueryRepo{}
+
+	sslToDelete, err := sslQueryRepo.GetSslPairById(sslId)
+	if err != nil {
+		return errors.New("SslNotFound")
+	}
+
+	vhostConfigFilePath, err := sslQueryRepo.GetVhostConfigFilePath(sslToDelete.Hostname)
+	if err != nil {
+		return err
+	}
+
+	vhostConfigContentStr, err := infraHelper.GetFileContent(vhostConfigFilePath.String())
+	if err != nil {
+		return err
+	}
+
+	sslBaseDirPath := "/speedia/pki/" + sslToDelete.Hostname.String()
+	err = os.RemoveAll(sslBaseDirPath)
+	if err != nil {
+		return err
+	}
+
+	vhostConfigVhsslMatch := regexp.MustCompile(`vhssl\s*\{[^}]*\}`)
+	vhostConfigWithoutVhssl := vhostConfigVhsslMatch.ReplaceAllString(vhostConfigContentStr, "")
+	vhostConfigWithoutSpaces := strings.TrimRightFunc(vhostConfigWithoutVhssl, unicode.IsSpace)
+	vhostConfigWithBreakLines := vhostConfigWithoutSpaces + "\n\n"
+
+	err = infraHelper.UpdateFile(
+		vhostConfigFilePath.String(),
+		vhostConfigWithBreakLines,
+		true,
+	)
+	return err
+}

+ 233 - 0
src/infra/sslQueryRepo.go

@@ -0,0 +1,233 @@
+package infra
+
+import (
+	"errors"
+	"log"
+	"regexp"
+	"strings"
+
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/valueObject"
+	infraHelper "github.com/speedianet/sam/src/infra/helper"
+)
+
+const olsHttpdConfigPath = "/usr/local/lsws/conf/httpd_config.conf"
+
+type SslQueryRepo struct{}
+
+type SslCertificates struct {
+	MainCertificate     entity.SslCertificate
+	ChainedCertificates []entity.SslCertificate
+}
+
+func (repo SslQueryRepo) GetVhosts() ([]valueObject.Fqdn, error) {
+	httpdContent, err := infraHelper.GetFileContent(olsHttpdConfigPath)
+	if err != nil {
+		return []valueObject.Fqdn{}, err
+	}
+
+	vhostsExpression := `virtualhost\s*(.*) {`
+	vhostsRegex := regexp.MustCompile(vhostsExpression)
+	vhostsMatch := vhostsRegex.FindAllStringSubmatch(httpdContent, -1)
+	if len(vhostsMatch) < 1 {
+		return []valueObject.Fqdn{}, err
+	}
+
+	httpdVhosts := []valueObject.Fqdn{}
+	for _, vhostMatchStr := range vhostsMatch {
+		if len(vhostMatchStr) < 2 {
+			continue
+		}
+
+		vhostStr := vhostMatchStr[1]
+		vhost, err := valueObject.NewFqdn(vhostStr)
+		if err != nil {
+			log.Printf("UnableToGetVhost (%v): %v", vhostStr, err)
+			continue
+		}
+		httpdVhosts = append(httpdVhosts, vhost)
+	}
+
+	return httpdVhosts, nil
+}
+
+func (repo SslQueryRepo) GetVhostConfigFilePath(
+	vhost valueObject.Fqdn,
+) (valueObject.UnixFilePath, error) {
+	var vhostConfigFilePath valueObject.UnixFilePath
+	httpdContent, err := infraHelper.GetFileContent(olsHttpdConfigPath)
+	if err != nil {
+		return "", err
+	}
+
+	vhostConfigFileExpression := `\s*configFile\s*(.*)`
+	vhostConfigFileMatch, err := infraHelper.GetRegexFirstGroup(httpdContent, vhostConfigFileExpression)
+	if err != nil {
+		return "", err
+	}
+
+	vhostConfigFilePath, err = valueObject.NewUnixFilePath(vhostConfigFileMatch)
+	if err != nil {
+		return "", err
+	}
+
+	return vhostConfigFilePath, nil
+}
+
+func (repo SslQueryRepo) SslCertificatesFactory(
+	sslCertContent valueObject.SslCertificateContent,
+) (SslCertificates, error) {
+	var certificates SslCertificates
+
+	sslCertContentSlice := strings.SplitAfter(
+		sslCertContent.String(),
+		"-----END CERTIFICATE-----\n",
+	)
+	for _, sslCertContentStr := range sslCertContentSlice {
+		certificateContent, err := valueObject.NewSslCertificateContent(sslCertContentStr)
+		if err != nil {
+			return certificates, err
+		}
+
+		certificate, err := entity.NewSslCertificate(certificateContent)
+		if err != nil {
+			return certificates, err
+		}
+
+		if !certificate.IsCA {
+			certificates.MainCertificate = certificate
+			continue
+		}
+
+		certificates.ChainedCertificates = append(certificates.ChainedCertificates, certificate)
+	}
+
+	return certificates, nil
+}
+
+func (repo SslQueryRepo) SslPairFactory(
+	sslHostname valueObject.Fqdn,
+	sslPrivateKey valueObject.SslPrivateKey,
+	sslCertificates SslCertificates,
+) (entity.SslPair, error) {
+	var ssl entity.SslPair
+
+	_, err := repo.GetVhostConfigFilePath(sslHostname)
+	if err != nil {
+		return ssl, err
+	}
+
+	certificate := sslCertificates.MainCertificate
+	chainCertificates := sslCertificates.ChainedCertificates
+
+	var chainCertificatesContent []valueObject.SslCertificateContent
+	for _, sslChainCertificate := range chainCertificates {
+		chainCertificatesContent = append(chainCertificatesContent, sslChainCertificate.Certificate)
+	}
+
+	hashId, err := valueObject.NewSslIdFromSslPairContent(
+		certificate.Certificate,
+		chainCertificatesContent,
+		sslPrivateKey,
+	)
+	if err != nil {
+		return ssl, err
+	}
+
+	return entity.NewSslPair(
+		hashId,
+		sslHostname,
+		certificate,
+		sslPrivateKey,
+		chainCertificates,
+	), nil
+}
+
+func (repo SslQueryRepo) GetSslPairs() ([]entity.SslPair, error) {
+	var sslPairs []entity.SslPair
+	httpdVhosts, err := repo.GetVhosts()
+	if err != nil {
+		return []entity.SslPair{}, err
+	}
+
+	for _, vhost := range httpdVhosts {
+		vhostConfigFilePath, err := repo.GetVhostConfigFilePath(vhost)
+		if err != nil {
+			return []entity.SslPair{}, err
+		}
+
+		vhostConfigContentStr, err := infraHelper.GetFileContent(vhostConfigFilePath.String())
+		if err != nil {
+			return []entity.SslPair{}, err
+		}
+
+		if len(vhostConfigContentStr) < 1 {
+			return []entity.SslPair{}, nil
+		}
+
+		vhostConfigKeyFileExpression := `keyFile\s*(.*)`
+		vhostConfigKeyFileMatch, err := infraHelper.GetRegexFirstGroup(vhostConfigContentStr, vhostConfigKeyFileExpression)
+		if err != nil {
+			return []entity.SslPair{}, nil
+		}
+		privateKeyContentStr, err := infraHelper.GetFileContent(vhostConfigKeyFileMatch)
+		if err != nil {
+			log.Printf("FailedToOpenHttpdFile: %v", err)
+			return []entity.SslPair{}, errors.New("FailedToOpenHttpdFile")
+		}
+		privateKey, err := valueObject.NewSslPrivateKey(privateKeyContentStr)
+		if err != nil {
+			return []entity.SslPair{}, nil
+		}
+
+		vhostConfigCertFileExpression := `certFile\s*(.*)`
+		vhostConfigCertFileMatch, err := infraHelper.GetRegexFirstGroup(vhostConfigContentStr, vhostConfigCertFileExpression)
+		if err != nil {
+			return []entity.SslPair{}, nil
+		}
+		certFileContentStr, err := infraHelper.GetFileContent(vhostConfigCertFileMatch)
+		if err != nil {
+			log.Printf("FailedToOpenVhconfFile: %v", err)
+			return []entity.SslPair{}, errors.New("FailedToOpenVhconfFile")
+		}
+		certificate, err := valueObject.NewSslCertificateContent(certFileContentStr)
+		if err != nil {
+			return []entity.SslPair{}, nil
+		}
+
+		sslCertificates, err := repo.SslCertificatesFactory(certificate)
+		if err != nil {
+			return []entity.SslPair{}, err
+		}
+
+		ssl, err := repo.SslPairFactory(vhost, privateKey, sslCertificates)
+		if err != nil {
+			return []entity.SslPair{}, err
+		}
+
+		sslPairs = append(sslPairs, ssl)
+	}
+
+	return sslPairs, nil
+}
+
+func (repo SslQueryRepo) GetSslPairById(sslId valueObject.SslId) (entity.SslPair, error) {
+	sslPairs, err := repo.GetSslPairs()
+	if err != nil {
+		return entity.SslPair{}, err
+	}
+
+	if len(sslPairs) < 1 {
+		return entity.SslPair{}, errors.New("SslPairNotFound")
+	}
+
+	for _, ssl := range sslPairs {
+		if ssl.Id.String() != sslId.String() {
+			continue
+		}
+
+		return ssl, nil
+	}
+
+	return entity.SslPair{}, errors.New("SslPairNotFound")
+}

+ 99 - 0
src/presentation/api/controller/sslController.go

@@ -0,0 +1,99 @@
+package apiController
+
+import (
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+	"github.com/speedianet/sam/src/domain/dto"
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/useCase"
+	"github.com/speedianet/sam/src/domain/valueObject"
+	"github.com/speedianet/sam/src/infra"
+	apiHelper "github.com/speedianet/sam/src/presentation/api/helper"
+)
+
+// GetSslPairs	 godoc
+// @Summary      GetSslPair
+// @Description  List ssl pairs.
+// @Tags         ssl
+// @Accept       json
+// @Produce      json
+// @Security     Bearer
+// @Success      200 {array} entity.SslPair
+// @Router       /ssl/ [get]
+func GetSslPairsController(c echo.Context) error {
+	sslQueryRepo := infra.SslQueryRepo{}
+	sslPairsList, err := useCase.GetSslPairs(sslQueryRepo)
+	if err != nil {
+		return apiHelper.ResponseWrapper(c, http.StatusInternalServerError, err.Error())
+	}
+
+	return apiHelper.ResponseWrapper(c, http.StatusOK, sslPairsList)
+}
+
+// AddSsl    	 godoc
+// @Summary      AddNewSslPair
+// @Description  Add a new ssl pair.
+// @Tags         ssl
+// @Accept       json
+// @Produce      json
+// @Security     Bearer
+// @Param        addSslPairDto 	  body    dto.AddSslPair  true  "NewSslPair"
+// @Success      201 {object} object{} "SslPairCreated"
+// @Router       /ssl/ [post]
+func AddSslPairController(c echo.Context) error {
+	requiredParams := []string{"hostname", "certificate", "key"}
+	requestBody, _ := apiHelper.GetRequestBody(c)
+
+	apiHelper.CheckMissingParams(requestBody, requiredParams)
+
+	sslCertificateContent := valueObject.NewSslCertificateContentPanic(requestBody["certificate"].(string))
+	sslCertificate := entity.NewSslCertificatePanic(sslCertificateContent)
+	sslPrivateKey := valueObject.NewSslPrivateKeyPanic(requestBody["key"].(string))
+
+	addSslPairDto := dto.NewAddSslPair(
+		valueObject.NewFqdnPanic(requestBody["hostname"].(string)),
+		sslCertificate,
+		sslPrivateKey,
+	)
+
+	sslCmdRepo := infra.SslCmdRepo{}
+
+	err := useCase.AddSslPair(
+		sslCmdRepo,
+		addSslPairDto,
+	)
+	if err != nil {
+		return apiHelper.ResponseWrapper(c, http.StatusInternalServerError, err.Error())
+	}
+
+	return apiHelper.ResponseWrapper(c, http.StatusCreated, "SslPairCreated")
+}
+
+// DeleteSsl	 godoc
+// @Summary      DeleteSslPair
+// @Description  Delete a ssl pair.
+// @Tags         ssl
+// @Accept       json
+// @Produce      json
+// @Security     Bearer
+// @Param        sslPairId 	  path   string  true  "SslPairId"
+// @Success      200 {object} object{} "SslPairDeleted"
+// @Router       /ssl/{sslPairId}/ [delete]
+func DeleteSslPairController(c echo.Context) error {
+	sslSerialNumber := valueObject.NewSslIdPanic(c.Param("sslPairId"))
+
+	sslQueryRepo := infra.SslQueryRepo{}
+	sslCmdRepo := infra.SslCmdRepo{}
+
+	err := useCase.DeleteSslPair(
+		sslQueryRepo,
+		sslCmdRepo,
+		sslSerialNumber,
+	)
+	if err != nil {
+		return apiHelper.ResponseWrapper(c, http.StatusInternalServerError, err.Error())
+	}
+
+	return apiHelper.ResponseWrapper(c, http.StatusOK, "SslPairDeleted")
+}

+ 179 - 0
src/presentation/api/docs/docs.go

@@ -776,9 +776,117 @@ const docTemplate = `{
                     }
                 }
             }
+        },
+        "/ssl/": {
+            "get": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "List ssls.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "GetSsls",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/entity.Ssl"
+                            }
+                        }
+                    }
+                }
+            },
+            "post": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "Add a new ssl.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "AddNewSsl",
+                "parameters": [
+                    {
+                        "description": "NewSsl",
+                        "name": "addSslDto",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/dto.AddSsl"
+                        }
+                    }
+                ],
+                "responses": {
+                    "201": {
+                        "description": "SslCreated",
+                        "schema": {
+                            "type": "object"
+                        }
+                    }
+                }
+            }
+        },
+        "/ssl/{sslId}/": {
+            "delete": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "Delete a ssl.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "DeleteSsl",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "SslId",
+                        "name": "sslId",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "SslDeleted",
+                        "schema": {
+                            "type": "object"
+                        }
+                    }
+                }
+            }
         }
     },
     "definitions": {
+        "big.Int": {
+            "type": "object"
+        },
         "dto.AddAccount": {
             "type": "object",
             "properties": {
@@ -832,6 +940,20 @@ const docTemplate = `{
                 }
             }
         },
+        "dto.AddSsl": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "$ref": "#/definitions/entity.SslPair"
+                },
+                "hostname": {
+                    "type": "string"
+                },
+                "key": {
+                    "$ref": "#/definitions/entity.SslPrivateKey"
+                }
+            }
+        },
         "dto.Login": {
             "type": "object",
             "properties": {
@@ -1110,6 +1232,60 @@ const docTemplate = `{
                 }
             }
         },
+        "entity.Ssl": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "$ref": "#/definitions/entity.SslPair"
+                },
+                "chainCertificates": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/entity.SslPair"
+                    }
+                },
+                "hostname": {
+                    "type": "string"
+                },
+                "id": {
+                    "$ref": "#/definitions/valueObject.SslId"
+                },
+                "key": {
+                    "$ref": "#/definitions/entity.SslPrivateKey"
+                }
+            }
+        },
+        "entity.SslPair": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "type": "string"
+                },
+                "commonName": {
+                    "type": "string"
+                },
+                "expiresAt": {
+                    "type": "string"
+                },
+                "isCA": {
+                    "type": "boolean"
+                },
+                "issuedAt": {
+                    "type": "string"
+                },
+                "serialNumber": {
+                    "$ref": "#/definitions/big.Int"
+                }
+            }
+        },
+        "entity.SslPrivateKey": {
+            "type": "object",
+            "properties": {
+                "key": {
+                    "type": "string"
+                }
+            }
+        },
         "valueObject.AccessTokenType": {
             "type": "string",
             "enum": [
@@ -1193,6 +1369,9 @@ const docTemplate = `{
                 "uninstalled",
                 "installed"
             ]
+        },
+        "valueObject.SslId": {
+            "type": "object"
         }
     },
     "securityDefinitions": {

+ 179 - 0
src/presentation/api/docs/swagger.json

@@ -770,9 +770,117 @@
                     }
                 }
             }
+        },
+        "/ssl/": {
+            "get": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "List ssls.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "GetSsls",
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/definitions/entity.Ssl"
+                            }
+                        }
+                    }
+                }
+            },
+            "post": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "Add a new ssl.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "AddNewSsl",
+                "parameters": [
+                    {
+                        "description": "NewSsl",
+                        "name": "addSslDto",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/dto.AddSsl"
+                        }
+                    }
+                ],
+                "responses": {
+                    "201": {
+                        "description": "SslCreated",
+                        "schema": {
+                            "type": "object"
+                        }
+                    }
+                }
+            }
+        },
+        "/ssl/{sslId}/": {
+            "delete": {
+                "security": [
+                    {
+                        "Bearer": []
+                    }
+                ],
+                "description": "Delete a ssl.",
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "ssl"
+                ],
+                "summary": "DeleteSsl",
+                "parameters": [
+                    {
+                        "type": "string",
+                        "description": "SslId",
+                        "name": "sslId",
+                        "in": "path",
+                        "required": true
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "SslDeleted",
+                        "schema": {
+                            "type": "object"
+                        }
+                    }
+                }
+            }
         }
     },
     "definitions": {
+        "big.Int": {
+            "type": "object"
+        },
         "dto.AddAccount": {
             "type": "object",
             "properties": {
@@ -826,6 +934,20 @@
                 }
             }
         },
+        "dto.AddSsl": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "$ref": "#/definitions/entity.SslPair"
+                },
+                "hostname": {
+                    "type": "string"
+                },
+                "key": {
+                    "$ref": "#/definitions/entity.SslPrivateKey"
+                }
+            }
+        },
         "dto.Login": {
             "type": "object",
             "properties": {
@@ -1104,6 +1226,60 @@
                 }
             }
         },
+        "entity.Ssl": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "$ref": "#/definitions/entity.SslPair"
+                },
+                "chainCertificates": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/entity.SslPair"
+                    }
+                },
+                "hostname": {
+                    "type": "string"
+                },
+                "id": {
+                    "$ref": "#/definitions/valueObject.SslId"
+                },
+                "key": {
+                    "$ref": "#/definitions/entity.SslPrivateKey"
+                }
+            }
+        },
+        "entity.SslPair": {
+            "type": "object",
+            "properties": {
+                "certificate": {
+                    "type": "string"
+                },
+                "commonName": {
+                    "type": "string"
+                },
+                "expiresAt": {
+                    "type": "string"
+                },
+                "isCA": {
+                    "type": "boolean"
+                },
+                "issuedAt": {
+                    "type": "string"
+                },
+                "serialNumber": {
+                    "$ref": "#/definitions/big.Int"
+                }
+            }
+        },
+        "entity.SslPrivateKey": {
+            "type": "object",
+            "properties": {
+                "key": {
+                    "type": "string"
+                }
+            }
+        },
         "valueObject.AccessTokenType": {
             "type": "string",
             "enum": [
@@ -1187,6 +1363,9 @@
                 "uninstalled",
                 "installed"
             ]
+        },
+        "valueObject.SslId": {
+            "type": "object"
         }
     },
     "securityDefinitions": {

+ 113 - 0
src/presentation/api/docs/swagger.yaml

@@ -1,5 +1,7 @@
 basePath: /v1
 definitions:
+  big.Int:
+    type: object
   dto.AddAccount:
     properties:
       password:
@@ -34,6 +36,15 @@ definitions:
       username:
         type: string
     type: object
+  dto.AddSsl:
+    properties:
+      certificate:
+        $ref: '#/definitions/entity.SslPair'
+      hostname:
+        type: string
+      key:
+        $ref: '#/definitions/entity.SslPrivateKey'
+    type: object
   dto.Login:
     properties:
       password:
@@ -214,6 +225,41 @@ definitions:
       uptimeSecs:
         type: number
     type: object
+  entity.Ssl:
+    properties:
+      certificate:
+        $ref: '#/definitions/entity.SslPair'
+      chainCertificates:
+        items:
+          $ref: '#/definitions/entity.SslPair'
+        type: array
+      hostname:
+        type: string
+      id:
+        $ref: '#/definitions/valueObject.SslId'
+      key:
+        $ref: '#/definitions/entity.SslPrivateKey'
+    type: object
+  entity.SslPair:
+    properties:
+      certificate:
+        type: string
+      commonName:
+        type: string
+      expiresAt:
+        type: string
+      isCA:
+        type: boolean
+      issuedAt:
+        type: string
+      serialNumber:
+        $ref: '#/definitions/big.Int'
+    type: object
+  entity.SslPrivateKey:
+    properties:
+      key:
+        type: string
+    type: object
   valueObject.AccessTokenType:
     enum:
     - sessionToken
@@ -274,6 +320,8 @@ definitions:
     - stopped
     - uninstalled
     - installed
+  valueObject.SslId:
+    type: object
 host: localhost:10000
 info:
   contact:
@@ -763,6 +811,71 @@ paths:
       summary: UpdateServiceStatus
       tags:
       - services
+  /ssl/:
+    get:
+      consumes:
+      - application/json
+      description: List ssls.
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            items:
+              $ref: '#/definitions/entity.Ssl'
+            type: array
+      security:
+      - Bearer: []
+      summary: GetSsls
+      tags:
+      - ssl
+    post:
+      consumes:
+      - application/json
+      description: Add a new ssl.
+      parameters:
+      - description: NewSsl
+        in: body
+        name: addSslDto
+        required: true
+        schema:
+          $ref: '#/definitions/dto.AddSsl'
+      produces:
+      - application/json
+      responses:
+        "201":
+          description: SslCreated
+          schema:
+            type: object
+      security:
+      - Bearer: []
+      summary: AddNewSsl
+      tags:
+      - ssl
+  /ssl/{sslId}/:
+    delete:
+      consumes:
+      - application/json
+      description: Delete a ssl.
+      parameters:
+      - description: SslId
+        in: path
+        name: sslId
+        required: true
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: SslDeleted
+          schema:
+            type: object
+      security:
+      - Bearer: []
+      summary: DeleteSsl
+      tags:
+      - ssl
 securityDefinitions:
   Bearer:
     description: Type "Bearer" + JWT token or API key.

+ 8 - 0
src/presentation/api/router.go

@@ -78,6 +78,13 @@ func servicesRoutes(baseRoute *echo.Group) {
 	servicesGroup.PUT("/", apiController.UpdateServiceController)
 }
 
+func sslRoutes(baseRoute *echo.Group) {
+	sslGroup := baseRoute.Group("/ssl")
+	sslGroup.GET("/", apiController.GetSslPairsController)
+	sslGroup.POST("/", apiController.AddSslPairController)
+	sslGroup.DELETE("/:sslPairId/", apiController.DeleteSslPairController)
+}
+
 func registerApiRoutes(baseRoute *echo.Group) {
 	swaggerRoute(baseRoute)
 	authRoutes(baseRoute)
@@ -87,4 +94,5 @@ func registerApiRoutes(baseRoute *echo.Group) {
 	runtimeRoutes(baseRoute)
 	accountRoutes(baseRoute)
 	servicesRoutes(baseRoute)
+	sslRoutes(baseRoute)
 }

+ 112 - 0
src/presentation/cli/controller/sslController.go

@@ -0,0 +1,112 @@
+package cliController
+
+import (
+	"github.com/speedianet/sam/src/domain/dto"
+	"github.com/speedianet/sam/src/domain/entity"
+	"github.com/speedianet/sam/src/domain/useCase"
+	"github.com/speedianet/sam/src/domain/valueObject"
+	"github.com/speedianet/sam/src/infra"
+	infraHelper "github.com/speedianet/sam/src/infra/helper"
+	cliHelper "github.com/speedianet/sam/src/presentation/cli/helper"
+	"github.com/spf13/cobra"
+)
+
+func GetSslPairsController() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "list",
+		Short: "GetSslPairs",
+		Run: func(cmd *cobra.Command, args []string) {
+			sslQueryRepo := infra.SslQueryRepo{}
+			sslPairsList, err := useCase.GetSslPairs(sslQueryRepo)
+			if err != nil {
+				cliHelper.ResponseWrapper(false, err.Error())
+			}
+
+			cliHelper.ResponseWrapper(true, sslPairsList)
+		},
+	}
+
+	return cmd
+}
+
+func AddSslPairController() *cobra.Command {
+	var hostnameStr string
+	var certificateFilePathStr string
+	var keyFilePathStr string
+
+	cmd := &cobra.Command{
+		Use:   "add",
+		Short: "AddNewSslPair",
+		Run: func(cmd *cobra.Command, args []string) {
+			certificateContentStr, err := infraHelper.GetFileContent(certificateFilePathStr)
+			if err != nil {
+				cliHelper.ResponseWrapper(false, "FailedToOpenSslCertificateFile")
+			}
+			sslCertificateContent := valueObject.NewSslCertificateContentPanic(certificateContentStr)
+
+			privateKeyContentStr, err := infraHelper.GetFileContent(keyFilePathStr)
+			if err != nil {
+				cliHelper.ResponseWrapper(false, "FailedToOpenPrivateKeyFile")
+			}
+
+			sslCertificate := entity.NewSslCertificatePanic(sslCertificateContent)
+			sslPrivateKey := valueObject.NewSslPrivateKeyPanic(privateKeyContentStr)
+
+			addSslDto := dto.NewAddSslPair(
+				valueObject.NewFqdnPanic(hostnameStr),
+				sslCertificate,
+				sslPrivateKey,
+			)
+
+			sslCmdRepo := infra.SslCmdRepo{}
+
+			err = useCase.AddSslPair(
+				sslCmdRepo,
+				addSslDto,
+			)
+			if err != nil {
+				cliHelper.ResponseWrapper(false, err.Error())
+			}
+
+			cliHelper.ResponseWrapper(true, "SslPairAdded")
+		},
+	}
+
+	cmd.Flags().StringVarP(&hostnameStr, "hostname", "t", "", "Hostname")
+	cmd.MarkFlagRequired("hostname")
+	cmd.Flags().StringVarP(&certificateFilePathStr, "certFilePath", "c", "", "CertificateFilePath")
+	cmd.MarkFlagRequired("certFilePath")
+	cmd.Flags().StringVarP(&keyFilePathStr, "keyFilePath", "k", "", "KeyFilePath")
+	cmd.MarkFlagRequired("keyFilePath")
+	return cmd
+}
+
+func DeleteSslPairController() *cobra.Command {
+	var sslPairIdStr string
+
+	cmd := &cobra.Command{
+		Use:   "delete",
+		Short: "DeleteSslPair",
+		Run: func(cmd *cobra.Command, args []string) {
+			sslId := valueObject.NewSslIdPanic(sslPairIdStr)
+
+			cronQueryRepo := infra.SslQueryRepo{}
+			cronCmdRepo := infra.SslCmdRepo{}
+
+			err := useCase.DeleteSslPair(
+				cronQueryRepo,
+				cronCmdRepo,
+				sslId,
+			)
+			if err != nil {
+				cliHelper.ResponseWrapper(false, err.Error())
+			}
+
+			cliHelper.ResponseWrapper(true, "SslPairDeleted")
+		},
+	}
+
+	cmd.Flags().StringVarP(&sslPairIdStr, "sslPairId", "s", "", "SslPairId")
+	cmd.MarkFlagRequired("sslPairId")
+	return cmd
+}

+ 13 - 0
src/presentation/cli/router.go

@@ -104,6 +104,18 @@ func servicesRoutes() {
 	servicesCmd.AddCommand(cliController.UpdateServiceController())
 }
 
+func sslRoutes() {
+	var sslCmd = &cobra.Command{
+		Use:   "ssl",
+		Short: "SslManagement",
+	}
+
+	rootCmd.AddCommand(sslCmd)
+	sslCmd.AddCommand(cliController.GetSslPairsController())
+	sslCmd.AddCommand(cliController.AddSslPairController())
+	sslCmd.AddCommand(cliController.DeleteSslPairController())
+}
+
 func registerCliRoutes() {
 	rootCmd.AddCommand(versionCmd)
 	rootCmd.AddCommand(serveCmd)
@@ -113,4 +125,5 @@ func registerCliRoutes() {
 	o11yRoutes()
 	runtimeRoutes()
 	servicesRoutes()
+	sslRoutes()
 }

+ 7 - 0
src/presentation/shared/checkEnvs.go

@@ -6,6 +6,7 @@ import (
 	"log"
 	"os"
 
+	"github.com/speedianet/sam/src/domain/valueObject"
 	"golang.org/x/exp/slices"
 )
 
@@ -61,4 +62,10 @@ func CheckEnvs() {
 			log.Fatalf("EnvWriteFileError: %v", err)
 		}
 	}
+
+	virtualHost := os.Getenv("VIRTUAL_HOST")
+	_, err = valueObject.NewFqdn(virtualHost)
+	if err != nil {
+		log.Fatalf("VirtualHostEnvInvalidValue")
+	}
 }