mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 11:40:04 +00:00
Certificate Authority work
This commit is contained in:
@@ -7,7 +7,11 @@
|
||||
"created_on",
|
||||
"modified_on",
|
||||
"name",
|
||||
"acme2_url"
|
||||
"acmesh_server",
|
||||
"ca_bundle",
|
||||
"max_domains",
|
||||
"is_wildcard_supported",
|
||||
"is_setup"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -27,10 +31,25 @@
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"acme2_url": {
|
||||
"acmesh_server": {
|
||||
"type": "string",
|
||||
"minLength": 8,
|
||||
"minLength": 2,
|
||||
"maxLength": 255
|
||||
},
|
||||
"ca_bundle": {
|
||||
"type": "string",
|
||||
"minLength": 0,
|
||||
"maxLength": 255
|
||||
},
|
||||
"max_domains": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"is_wildcard_supported": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_setup": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
@@ -49,6 +49,9 @@
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"certificate_authority": {
|
||||
"$ref": "#/components/schemas/CertificateAuthorityObject"
|
||||
},
|
||||
"dns_provider_id": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
|
@@ -37,10 +37,14 @@
|
||||
"value": {
|
||||
"result": {
|
||||
"id": 1,
|
||||
"created_on": 1602588511,
|
||||
"modified_on": 1602588511,
|
||||
"name": "Let's Encrypt",
|
||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
||||
"created_on": 1627531400,
|
||||
"modified_on": 1627531400,
|
||||
"name": "ZeroSSL",
|
||||
"acmesh_server": "zerossl",
|
||||
"ca_bundle": "",
|
||||
"max_domains": 10,
|
||||
"is_wildcard_supported": true,
|
||||
"is_setup": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -46,10 +46,14 @@
|
||||
"value": {
|
||||
"result": {
|
||||
"id": 1,
|
||||
"created_on": 1602588511,
|
||||
"modified_on": 1602588511,
|
||||
"name": "Let's Encrypt",
|
||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
||||
"created_on": 1627531400,
|
||||
"modified_on": 1627531400,
|
||||
"name": "ZeroSSL",
|
||||
"acmesh_server": "zerossl",
|
||||
"ca_bundle": "",
|
||||
"max_domains": 10,
|
||||
"is_wildcard_supported": true,
|
||||
"is_setup": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -64,17 +64,25 @@
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"created_on": 1602588511,
|
||||
"modified_on": 1602588511,
|
||||
"name": "Let's Encrypt",
|
||||
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
|
||||
"created_on": 1627531400,
|
||||
"modified_on": 1627531400,
|
||||
"name": "ZeroSSL",
|
||||
"acmesh_server": "zerossl",
|
||||
"ca_bundle": "",
|
||||
"max_domains": 10,
|
||||
"is_wildcard_supported": true,
|
||||
"is_setup": false
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"created_on": 1602588511,
|
||||
"modified_on": 1602588511,
|
||||
"name": "Let's Encrypt (Staging)",
|
||||
"acme2_url": "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
"created_on": 1627531400,
|
||||
"modified_on": 1627531400,
|
||||
"name": "Let's Encrypt",
|
||||
"acmesh_server": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"ca_bundle": "",
|
||||
"max_domains": 10,
|
||||
"is_wildcard_supported": true,
|
||||
"is_setup": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -32,11 +32,15 @@
|
||||
"default": {
|
||||
"value": {
|
||||
"result": {
|
||||
"id": 3,
|
||||
"created_on": 1602588900,
|
||||
"modified_on": 1602588900,
|
||||
"name": "Boulder",
|
||||
"acme2_url": "https://boulder.local/directory"
|
||||
"id": 1,
|
||||
"created_on": 1627531400,
|
||||
"modified_on": 1627531400,
|
||||
"name": "ZeroSSL",
|
||||
"acmesh_server": "zerossl",
|
||||
"ca_bundle": "",
|
||||
"max_domains": 10,
|
||||
"is_wildcard_supported": true,
|
||||
"is_setup": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -56,7 +56,11 @@ CREATE TABLE IF NOT EXISTS `certificate_authority`
|
||||
created_on INTEGER NOT NULL DEFAULT 0,
|
||||
modified_on INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL,
|
||||
acme2_url TEXT NOT NULL,
|
||||
acmesh_server TEXT NOT NULL DEFAULT "",
|
||||
is_setup INTEGER NOT NULL DEFAULT 0,
|
||||
ca_bundle TEXT NOT NULL DEFAULT "",
|
||||
is_wildcard_supported INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards
|
||||
max_domains INTEGER NOT NULL DEFAULT 5, -- per request
|
||||
is_deleted INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
|
@@ -36,20 +36,51 @@ INSERT INTO `certificate_authority` (
|
||||
created_on,
|
||||
modified_on,
|
||||
name,
|
||||
acme2_url
|
||||
acmesh_server,
|
||||
is_wildcard_supported,
|
||||
max_domains
|
||||
) VALUES (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"Let's Encrypt",
|
||||
"https://acme-v02.api.letsencrypt.org/directory"
|
||||
"ZeroSSL",
|
||||
"zerossl",
|
||||
1,
|
||||
10
|
||||
), (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"Let's Encrypt (Staging)",
|
||||
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
"Let's Encrypt",
|
||||
"https://acme-v02.api.letsencrypt.org/directory",
|
||||
1,
|
||||
10
|
||||
), (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"Buypass Go SSL",
|
||||
"https://api.buypass.com/acme/directory",
|
||||
0,
|
||||
5
|
||||
), (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"Let's Encrypt (Testing)",
|
||||
"https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
1,
|
||||
10
|
||||
), (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"Buypass Go SSL (Testing)",
|
||||
"https://api.test4.buypass.no/acme/directory",
|
||||
0,
|
||||
5
|
||||
), (
|
||||
strftime('%s', 'now'),
|
||||
strftime('%s', 'now'),
|
||||
"SSL.com",
|
||||
"ssl.com",
|
||||
0,
|
||||
10
|
||||
);
|
||||
|
||||
|
||||
-- migrate:down
|
||||
|
||||
-- Not allowed to go down from initial
|
||||
|
@@ -17,7 +17,7 @@ var acmeShFile string
|
||||
|
||||
// GetAcmeShVersion will return the acme.sh script version
|
||||
func GetAcmeShVersion() string {
|
||||
if r, err := acmeShExec("--version"); err == nil {
|
||||
if r, err := shExec("--version"); err == nil {
|
||||
// modify the output
|
||||
r = strings.Trim(r, "\n")
|
||||
v := strings.Split(r, "\n")
|
||||
@@ -26,13 +26,15 @@ func GetAcmeShVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func acmeShExec(args ...string) (string, error) {
|
||||
// shExec executes the acme.sh with arguments
|
||||
func shExec(args ...string) (string, error) {
|
||||
if _, err := os.Stat(acmeShFile); os.IsNotExist(err) {
|
||||
e := fmt.Errorf("%s does not exist", acmeShFile)
|
||||
logger.Error("AcmeShError", e)
|
||||
return "", e
|
||||
}
|
||||
|
||||
logger.Debug("CMD: %s %v", acmeShFile, args)
|
||||
// nolint: gosec
|
||||
c := exec.Command(acmeShFile, args...)
|
||||
b, e := c.Output()
|
||||
@@ -61,3 +63,33 @@ func WriteAcmeSh() {
|
||||
logger.Info("Wrote %s", acmeShFile)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestCert does all the heavy lifting
|
||||
func RequestCert(domains []string, method string) error {
|
||||
args := []string{"--issue"}
|
||||
|
||||
webroot := "/home/wwwroot/example.com"
|
||||
|
||||
// Add domains to args
|
||||
for _, domain := range domains {
|
||||
args = append(args, "-d", domain)
|
||||
}
|
||||
|
||||
switch method {
|
||||
// case "dns":
|
||||
case "http":
|
||||
args = append(args, "-w", webroot)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("RequestCert method not supported: %s", method)
|
||||
}
|
||||
|
||||
ret, err := shExec(args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("ret: %+v", ret)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -10,12 +10,14 @@ func CreateCertificateAuthority() string {
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"acme2_url"
|
||||
"acmesh_server",
|
||||
"max_domains"
|
||||
],
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acme2_url": %s
|
||||
"acmesh_server": %s,
|
||||
"max_domains": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
||||
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
|
||||
}
|
||||
|
@@ -10,8 +10,9 @@ func UpdateCertificateAuthority() string {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": %s,
|
||||
"acme2_url": %s
|
||||
"acmesh_server": %s,
|
||||
"max_domains": %s
|
||||
}
|
||||
}
|
||||
`, stringMinMax(1, 100), stringMinMax(8, 255))
|
||||
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
|
||||
}
|
||||
|
@@ -152,13 +152,11 @@ func GetByStatus(status string) ([]Model, error) {
|
||||
SELECT
|
||||
t.*
|
||||
FROM "%s" t
|
||||
INNER JOIN "dns_provider" d ON d."id" = t."dns_provider_id"
|
||||
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
|
||||
WHERE
|
||||
t."type" IN ("http", "dns") AND
|
||||
t."status" = ? AND
|
||||
t."certificate_authority_id" > 0 AND
|
||||
t."dns_provider_id" > 0 AND
|
||||
t."is_deleted" = 0
|
||||
`, tableName)
|
||||
|
||||
|
@@ -2,11 +2,14 @@ package certificate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"npm/internal/acme"
|
||||
"npm/internal/database"
|
||||
"npm/internal/entity/certificateauthority"
|
||||
"npm/internal/entity/dnsprovider"
|
||||
"npm/internal/logger"
|
||||
"npm/internal/types"
|
||||
)
|
||||
|
||||
@@ -86,6 +89,10 @@ func (m *Model) Save() error {
|
||||
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
|
||||
}
|
||||
|
||||
if !m.ValidateWildcardSupport() {
|
||||
return fmt.Errorf("Cannot use Wildcard domains with this CA")
|
||||
}
|
||||
|
||||
m.setDefaultStatus()
|
||||
|
||||
if m.ID == 0 {
|
||||
@@ -129,6 +136,32 @@ func (m *Model) Validate() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateWildcardSupport will ensure that the CA given supports wildcards,
|
||||
// only if the domains on this object have at least 1 wildcard
|
||||
func (m *Model) ValidateWildcardSupport() bool {
|
||||
domains, err := m.DomainNames.AsStringArray()
|
||||
if err != nil {
|
||||
logger.Error("ValidateWildcardSupportError", err)
|
||||
return false
|
||||
}
|
||||
|
||||
hasWildcard := false
|
||||
for _, domain := range domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
hasWildcard = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasWildcard {
|
||||
m.Expand()
|
||||
if !m.CertificateAuthority.IsWildcardSupported {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) setDefaultStatus() {
|
||||
if m.ID == 0 {
|
||||
// It's a new certificate
|
||||
@@ -154,23 +187,33 @@ func (m *Model) Expand() {
|
||||
|
||||
// Request makes a certificate request
|
||||
func (m *Model) Request() error {
|
||||
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
|
||||
|
||||
m.Expand()
|
||||
m.Status = StatusRequesting
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If error
|
||||
m.Status = StatusFailed
|
||||
m.ErrorMessage = "something"
|
||||
if err := m.Save(); err != nil {
|
||||
// do request
|
||||
domains, err := m.DomainNames.AsStringArray()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = acme.RequestCert(domains, m.Type)
|
||||
if err != nil {
|
||||
m.Status = StatusFailed
|
||||
m.ErrorMessage = err.Error()
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If done
|
||||
m.Status = StatusProvided
|
||||
t := time.Now()
|
||||
m.ExpiresOn.Time = &t
|
||||
m.ExpiresOn.Time = &t // todo
|
||||
if err := m.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -33,13 +33,21 @@ func Create(ca *Model) (int, error) {
|
||||
created_on,
|
||||
modified_on,
|
||||
name,
|
||||
acme2_url,
|
||||
acmesh_server,
|
||||
ca_bundle,
|
||||
max_domains,
|
||||
is_wildcard_supported,
|
||||
is_setup,
|
||||
is_deleted
|
||||
) VALUES (
|
||||
:created_on,
|
||||
:modified_on,
|
||||
:name,
|
||||
:acme2_url,
|
||||
:acmesh_server,
|
||||
:ca_bundle,
|
||||
:max_domains,
|
||||
:is_wildcard_supported,
|
||||
:is_setup,
|
||||
:is_deleted
|
||||
)`, ca)
|
||||
|
||||
@@ -69,7 +77,11 @@ func Update(ca *Model) error {
|
||||
created_on = :created_on,
|
||||
modified_on = :modified_on,
|
||||
name = :name,
|
||||
acme2_url = :acme2_url,
|
||||
acmesh_server = :acmesh_server,
|
||||
ca_bundle = :ca_bundle,
|
||||
max_domains = :max_domains,
|
||||
is_wildcard_supported = :is_wildcard_supported,
|
||||
is_setup = :is_setup,
|
||||
is_deleted = :is_deleted
|
||||
WHERE id = :id`, ca)
|
||||
|
||||
|
@@ -14,12 +14,16 @@ const (
|
||||
|
||||
// Model is the user model
|
||||
type Model struct {
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
Acme2URL string `json:"acme2_url" db:"acme2_url" filter:"acme2_url,string"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
ID int `json:"id" db:"id" filter:"id,integer"`
|
||||
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
|
||||
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
|
||||
Name string `json:"name" db:"name" filter:"name,string"`
|
||||
AcmeshServer string `json:"acmesh_server" db:"acmesh_server" filter:"acmesh_server,string"`
|
||||
CABundle string `json:"ca_bundle" db:"ca_bundle" filter:"ca_bundle,string"`
|
||||
MaxDomains int `json:"max_domains" db:"max_domains" filter:"max_domains,integer"`
|
||||
IsWildcardSupported bool `json:"is_wildcard_supported" db:"is_wildcard_supported" filter:"is_wildcard_supported,boolean"`
|
||||
IsSetup bool `json:"is_setup" db:"is_setup" filter:"is_setup,boolean"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
|
||||
}
|
||||
|
||||
func (m *Model) getByQuery(query string, params []interface{}) error {
|
||||
|
@@ -56,3 +56,16 @@ func (j *JSONB) UnmarshalJSON(data []byte) error {
|
||||
func (j JSONB) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(j.Decoded)
|
||||
}
|
||||
|
||||
// AsStringArray will attempt to return as []string
|
||||
func (j JSONB) AsStringArray() ([]string, error) {
|
||||
var strs []string
|
||||
|
||||
// Encode then Decode onto this type
|
||||
b, _ := j.MarshalJSON()
|
||||
if err := json.Unmarshal(b, &strs); err != nil {
|
||||
return strs, err
|
||||
}
|
||||
|
||||
return strs, nil
|
||||
}
|
||||
|
@@ -41,12 +41,14 @@ mainLoop:
|
||||
break mainLoop
|
||||
}
|
||||
case <-ticker.C:
|
||||
// Can confirm that this will wait for completion before the next loop
|
||||
requestCertificates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestCertificates() {
|
||||
// logger.Debug("requestCertificates fired")
|
||||
rows, err := certificate.GetByStatus(certificate.StatusReady)
|
||||
if err != nil {
|
||||
logger.Error("requestCertificatesError", err)
|
||||
|
Reference in New Issue
Block a user