Remove email details from admin pages

As Let's Encrypt has removed all email sending from boulder, we no longer need
the email details in LabCA either.
This commit is contained in:
Arjan H
2025-08-05 18:14:37 +02:00
parent 24770e3c80
commit e875804af1
7 changed files with 2 additions and 395 deletions

View File

@@ -93,11 +93,11 @@ echo "LABCA_IMAGE_VERSION=v25.03" > labca.env
## Usage
Once LabCA has been setup you should go through the admin pages and e.g. configure the email details for outgoing notifications. Now your instance is ready to provide HTTPS certificates for your internal applications.
Once LabCA has been setup, your instance is ready to provide HTTPS certificates for your internal applications.
### Admin
The admin section is only accessible to the user account created at the start of the setup. The [dashboard](https://user-images.githubusercontent.com/44847421/48658726-ebd4c400-ea46-11e8-8cb1-43584dbc3719.jpg) gives an overview of the current status of your LabCA instance. Via the menu you can navigate to the details of your ACME objects such as the certificates, to several system logfiles and to the various management tasks such as backup/restore, email settings and changing your password.
The admin section is only accessible to the user account created at the start of the setup. The [dashboard](https://user-images.githubusercontent.com/44847421/48658726-ebd4c400-ea46-11e8-8cb1-43584dbc3719.jpg) gives an overview of the current status of your LabCA instance. Via the menu you can navigate to the details of your ACME objects such as the certificates, to several system logfiles and to the various management tasks such as backup/restore and changing your password.
These screenshots give a preview of the admin section:

View File

@@ -61,7 +61,6 @@ COPY tmp/checkrenew /opt/labca/
COPY tmp/commander /opt/labca/
COPY tmp/control.sh /opt/labca/
COPY tmp/cron_d /opt/labca/
COPY tmp/mailer /opt/labca/
COPY tmp/renew /opt/labca/
COPY tmp/restore /opt/labca/
COPY tmp/utils.sh /opt/labca/

View File

@@ -59,7 +59,6 @@ cp -rp $cloneDir/checkrenew $TMP_DIR/
cp -rp $cloneDir/commander $TMP_DIR/
cp -rp $cloneDir/control_do.sh $TMP_DIR/control.sh
cp -rp $cloneDir/cron_d $TMP_DIR/
cp -rp $cloneDir/mailer $TMP_DIR/
cp -rp $cloneDir/renew $TMP_DIR/
cp -rp $cloneDir/restore $TMP_DIR/
cp -rp $cloneDir/utils.sh $TMP_DIR/

View File

@@ -165,9 +165,6 @@ case $txt in
# NOTE: admin is one of the few commands that is NOT a subcommand of boulder!
docker compose exec boulder bin/admin -config labca/config/admin.json revoke-cert -serial $serial -reason $reason -dry-run=false 2>&1
;;
"test-email")
read recipient
;;
"boulder-start")
cd /opt/boulder
COMPOSE_HTTP_TIMEOUT=120 docker compose up -d bmysql bconsul bpkimetal bredis

View File

@@ -30,36 +30,6 @@ if [ "$extended_timeout" != "" ]; then
else
PKI_EXTENDED_TIMEOUT=0
fi
enabled=$(grep "email\": {" $dataDir/config.json -A1 | grep enable | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
if [ "$enabled" == "true," ]; then
PKI_EMAIL_SERVER=$(grep server $dataDir/config.json | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_EMAIL_PORT=$(grep port $dataDir/config.json | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_EMAIL_USER=$(grep user $dataDir/config.json | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_EMAIL_PASS=$(grep pass $dataDir/config.json | grep -v password | head -1 | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
pwd=""
if [ -e $baseDir/bin/labca-gui ]; then
pwd=$([ -e ] && $baseDir/bin/labca-gui -config $dataDir/config.json -d $PKI_EMAIL_PASS || echo "")
elif [ -e $baseDir/bin/labca-gui_prev ]; then
pwd=$([ -e ] && $baseDir/bin/labca-gui_prev -config $dataDir/config.json -d $PKI_EMAIL_PASS || echo "")
fi
PKI_EMAIL_PASS=$pwd
PKI_EMAIL_FROM=$(grep from $dataDir/config.json | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
PKI_EMAIL_TRUST=$(grep trust_root $dataDir/config.json | perl -p0e 's/.*?:\s+(.*)/\1/' | sed -e 's/\",//g' | sed -e 's/\"//g')
if [ "$PKI_EMAIL_TRUST" == "private" ]; then
PKI_EMAIL_TRUST="labca/certs/webpki/root-01-cert.pem"
elif [ "$PKI_EMAIL_TRUST" == "skip" ]; then
PKI_EMAIL_TRUST="InsecureSkipVerify"
else
PKI_EMAIL_TRUST=""
fi
else
PKI_EMAIL_SERVER="localhost"
PKI_EMAIL_PORT="9380"
PKI_EMAIL_USER="cert-manager@example.com"
PKI_EMAIL_PASS="password"
PKI_EMAIL_FROM="Expiry bot <test@example.com>"
PKI_EMAIL_TRUST="labca/certs/ipki/minica.pem"
fi
perl -i -p0e "s/(\"dnsStaticResolvers\": \[\n).*?(\s+\],)/\1\t\t\t\"$PKI_DNS\"\2/igs" config/remoteva-a.json
@@ -207,12 +177,6 @@ else
fi
sed -i -e "s/\"timeout\": \"1s\"/\"timeout\": \"5s\"/" config/health-checker.json
sed -i -e "s/\"server\": \".*\"/\"server\": \"$PKI_EMAIL_SERVER\"/" config/bad-key-revoker.json
sed -i -e "s/\"port\": \".*\"/\"port\": \"$PKI_EMAIL_PORT\"/" config/bad-key-revoker.json
sed -i -e "s/\"username\": \".*\"/\"username\": \"$PKI_EMAIL_USER\"/" config/bad-key-revoker.json
sed -i -e "s/\"from\": \".*\"/\"from\": \"$PKI_EMAIL_FROM\"/" config/bad-key-revoker.json
sed -i -e "s|\"SMTPTrustedRootFile\": \".*\"|\"SMTPTrustedRootFile\": \"$PKI_EMAIL_TRUST\"|" config/bad-key-revoker.json
sed -i -e "s/\"purgeInterval\": \".*\"/\"purgeInterval\": \"1s\"/" config/akamai-purger.json
for fl in $(grep -Rl maxOpenConns config/); do
@@ -224,10 +188,6 @@ for fl in $(grep -Rl maxOpenConns config/); do
fi
done
if [ "$PKI_EMAIL_PASS" != "" ]; then
sed -i -e "s/.*/$PKI_EMAIL_PASS/" secrets/smtp_password
fi
rm -f test-ca.key
rm -f test-ca.key.der
rm -f test-ca.pem

View File

@@ -813,167 +813,6 @@ func _crlIntervalUpdateHandler(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(res)
}
// EmailConfig stores configuration used for sending out emails
type EmailConfig struct {
DoEmail bool
Server string
Port string
EmailUser string
EmailPwd []byte
From string
TrustRoot string
Errors map[string]string
}
// Validate that the email config is valid and complete
func (cfg *EmailConfig) Validate() bool {
cfg.Errors = make(map[string]string)
result, err := _encrypt(cfg.EmailPwd)
if err == nil {
cfg.EmailPwd = []byte(result)
} else {
cfg.Errors["EmailPwd"] = "Could not encrypt this password: " + err.Error()
}
if !cfg.DoEmail {
return len(cfg.Errors) == 0
}
if strings.TrimSpace(cfg.Server) == "" {
cfg.Errors["Server"] = "Please enter the email server address"
}
if strings.TrimSpace(cfg.Port) == "" {
cfg.Errors["Port"] = "Please enter the email server port number"
}
p, err := strconv.Atoi(cfg.Port)
if err != nil {
cfg.Errors["Port"] = "Port number must be numeric"
} else if p <= 0 {
cfg.Errors["Port"] = "Port number must be positive"
} else if p > 65535 {
cfg.Errors["Port"] = "Port number too large"
}
if strings.TrimSpace(cfg.EmailUser) == "" {
cfg.Errors["EmailUser"] = "Please enter the username for authorization to the email server"
}
res, err := _decrypt(string(cfg.EmailPwd))
if err != nil {
cfg.Errors["EmailPwd"] = "Could not decrypt this password: " + err.Error()
}
if strings.TrimSpace(string(res)) == "" {
cfg.Errors["EmailPwd"] = "Please enter the password for authorization to the email server"
}
if strings.TrimSpace(cfg.From) == "" {
cfg.Errors["From"] = "Please enter the from email address"
}
if strings.TrimSpace(cfg.TrustRoot) == "" {
cfg.Errors["From"] = "Please select what root CA to trust for validating the email server certificate"
}
return len(cfg.Errors) == 0
}
func _emailUpdateHandler(w http.ResponseWriter, r *http.Request) {
cfg := &EmailConfig{
DoEmail: (r.Form.Get("do_email") == "true"),
Server: r.Form.Get("server"),
Port: r.Form.Get("port"),
EmailUser: r.Form.Get("email_user"),
EmailPwd: []byte(r.Form.Get("email_pwd")),
From: r.Form.Get("from"),
TrustRoot: r.Form.Get("trust_root"),
}
res := makeErrorsResponse(true)
if cfg.Validate() {
delta := false
if cfg.DoEmail != viper.GetBool("labca.email.enable") {
delta = true
viper.Set("labca.email.enable", cfg.DoEmail)
}
if cfg.Server != viper.GetString("labca.email.server") {
delta = true
viper.Set("labca.email.server", cfg.Server)
}
if cfg.Port != viper.GetString("labca.email.port") {
delta = true
viper.Set("labca.email.port", cfg.Port)
}
if cfg.EmailUser != viper.GetString("labca.email.user") {
delta = true
viper.Set("labca.email.user", cfg.EmailUser)
}
res1, err1 := _decrypt(string(cfg.EmailPwd))
if err1 != nil && cfg.DoEmail {
log.Println("WARNING: could not decrypt given password: " + err1.Error())
}
res2, err2 := _decrypt(viper.GetString("labca.email.pass"))
if err2 != nil && cfg.DoEmail && viper.GetString("labca.email.pass") != "" {
log.Println("WARNING: could not decrypt stored password: " + err2.Error())
}
if string(res1) != string(res2) {
delta = true
viper.Set("labca.email.pass", string(cfg.EmailPwd))
}
if cfg.From != viper.GetString("labca.email.from") {
delta = true
viper.Set("labca.email.from", cfg.From)
}
if cfg.TrustRoot != viper.GetString("labca.email.trust_root") {
delta = true
viper.Set("labca.email.trust_root", cfg.TrustRoot)
}
if delta {
_ = viper.WriteConfig()
err := _applyConfig()
if err != nil {
res.Success = false
res.Errors = cfg.Errors
res.Errors["EmailUpdate"] = "Config apply error: '" + err.Error() + "'"
}
} else {
res.Success = false
res.Errors = cfg.Errors
res.Errors["EmailUpdate"] = "Nothing changed!"
}
} else {
res.Success = false
res.Errors = cfg.Errors
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(res)
}
func _emailSendHandler(w http.ResponseWriter, r *http.Request) {
res := makeErrorsResponse(true)
recipient := viper.GetString("user.email")
if _hostCommand(w, r, "test-email", recipient) {
// Only on success, as when this returns false for this case the response has already been sent!
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(res)
}
}
func _exportHandler(w http.ResponseWriter, r *http.Request) {
certname := r.Form.Get("certname")
certFile := fmt.Sprintf("%s%s.pem", CERT_FILES_PATH, certname)
@@ -1281,16 +1120,6 @@ func _managePostDispatch(w http.ResponseWriter, r *http.Request, action string)
return true
}
if action == "update-email" {
_emailUpdateHandler(w, r)
return true
}
if action == "send-email" {
_emailSendHandler(w, r)
return true
}
if action == "version-check" {
_checkUpdatesHandler(w, r)
return true
@@ -1369,8 +1198,6 @@ func _managePost(w http.ResponseWriter, r *http.Request) {
"update-backend",
"update-config",
"update-crl-interval",
"update-email",
"send-email",
"version-check",
"version-update",
"upload-root-crl",
@@ -1620,23 +1447,6 @@ func _manageGet(w http.ResponseWriter, r *http.Request) {
manageData["WhitelistDomains"] = viper.GetString("labca.whitelist")
}
manageData["ExtendedTimeout"] = viper.GetBool("labca.extended_timeout")
manageData["DoEmail"] = viper.GetBool("labca.email.enable")
manageData["Server"] = viper.GetString("labca.email.server")
manageData["Port"] = viper.GetInt("labca.email.port")
manageData["EmailUser"] = viper.GetString("labca.email.user")
manageData["EmailPwd"] = ""
if viper.Get("labca.email.pass") != nil {
pwd := viper.GetString("labca.email.pass")
result, err := _decrypt(pwd)
if err == nil {
manageData["EmailPwd"] = string(result)
} else {
log.Printf("WARNING: could not decrypt email password: %s!\n", err.Error())
}
}
manageData["From"] = viper.GetString("labca.email.from")
manageData["TrustRoot"] = viper.GetString("labca.email.trust_root")
}
manageData["Name"] = viper.GetString("user.name")
@@ -2307,18 +2117,6 @@ func _hostCommand(w http.ResponseWriter, r *http.Request, command string, params
}
log.Printf("ERROR: Message from server: '%s'", message)
if command == "test-email" {
// Want special error handling for this case
res := makeErrorsResponse(false)
if strings.Contains(string(message), "certificate signed by unknown authority") {
res.Errors["EmailSend"] = "Error: SMTP server certificate signed by unknown authority"
} else {
res.Errors["EmailSend"] = "Failed to send email - see logs"
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(res)
return false
}
errorHandler(w, r, errors.New(string(message)), http.StatusInternalServerError)
return false
}

View File

@@ -23,9 +23,6 @@
<li class="">
<a data-toggle="tab" href="#config">Config</a>
</li>
<li class="">
<a data-toggle="tab" href="#email">Email</a>
</li>
{{ end }}
<li class="">
<a data-toggle="tab" href="#account">Account</a>
@@ -318,54 +315,6 @@
</form>
</div>
<div class="tab-pane fade" id="email">
<br/>
<form role="form">
<div class="form-group">
<input type="checkbox" class="email-cb" id="email-cb" value="enable" {{ if .DoEmail }}checked{{ end }}></input>
Send emails about upcoming certificate expiration
</div>
<div class="form-group">
<label for="server">Server:</label>
<input class="form-control non-fluid" type="text" id="server" name="server" value="{{ .Server }}">
<span class="error email-error hidden" id="server-error"></span>
</div>
<div class="form-group">
<label for="port">Port:</label>
<input class="form-control non-fluid" type="text" id="port" name="port" value="{{ .Port }}">
<span class="error email-error hidden" id="port-error"></span>
</div>
<div class="form-group">
<label for="emailuser">Username:</label>
<input class="form-control non-fluid" type="text" id="emailuser" name="emailuser" value="{{ .EmailUser }}">
<span class="error email-error hidden" id="emailuser-error"></span>
</div>
<div class="form-group">
<label for="emailpwd">Password:</label>
<input class="form-control non-fluid" type="password" name="emailpwd" id="emailpwd" value="{{ .EmailPwd }}">
<span class="error email-error hidden" id="emailpwd-error"></span>
</div>
<div class="form-group">
<label for="from">From address:</label>
<input class="form-control non-fluid" type="text" id="from" name="from" value="{{ .From }}">
<span class="error email-error hidden" id="from-error"></span>
</div>
<div class="form-group">
<label for="rootfile">Root CAs for validating email server certficate:</label><br/>
<input type="radio" class="rootfile-rd" id="rootfile-public" name="rootfile-type" value="public" {{ if and (ne .TrustRoot "private") (ne .TrustRoot "skip")}}checked{{ end }}/> Trust standard public root CAs<br/>
<input type="radio" class="rootfile-rd" id="rootfile-private" name="rootfile-type" value="private" {{ if eq .TrustRoot "private"}}checked{{ end }}/> Trust the LabCA root CA<br/>
<input type="radio" class="rootfile-rd" id="rootfile-skip" name="rootfile-type" value="skip" {{ if eq .TrustRoot "skip"}}checked{{ end }}/> Do not check server certificate<br/>
</div>
<div class="form-group">
<span class="hidden" id="update-email-result"></span>
<button class="btn btn-default" type="button" id="update-email" title="Update the email configuration">Update</button>
</div>
<div class="form-group">
<span class="hidden" id="send-email-result"></span>
<button class="btn btn-default" type="button" id="send-email" title="Send a test email to the administrator">Send Test Email</button>
</div>
</form>
</div>
{{ end }}
<div class="tab-pane fade" id="account">
@@ -665,9 +614,6 @@
$("#update-account-result").hide();
$(".config-error").hide();
$("#update-config-result").hide();
$(".email-error").hide();
$("#update-email-result").hide();
$("#send-email-result").hide();
$("#root-crl-result").hide();
$("#issuer-crl-result").hide();
$("#crl-interval-result").hide();
@@ -1030,83 +976,6 @@
$("#update-config-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "update-email") {
form = $(evt.target).parent().parent()[0]
if (!form.checkValidity()) {
$('#modal-spinner').modal('hide');
const tmpSubmit = document.createElement('button')
form.appendChild(tmpSubmit)
tmpSubmit.click()
form.removeChild(tmpSubmit)
} else {
$.ajax(window.location.href, {
method: "POST",
data: {
action: $(evt.target).attr("id"),
do_email: $("#email-cb").prop("checked"),
server: $("#server").val(),
port: $("#port").val(),
email_user: $("#emailuser").val(),
email_pwd: $("#emailpwd").val(),
from: $("#from").val(),
trust_root: $("#rootfile-private").prop('checked') ? "private" : ($("#rootfile-skip").prop('checked') ? "skip" : "public"),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully updated configuration.<br/>";
$("#update-email-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else {
jQuery.each(data.Errors, function(nm, val) {
nm = nm.toLowerCase();
if (nm == "emailupdate") {
$("#update-email-result").removeClass("hidden").removeClass("success").show().html(val + "<br/>").addClass("error");
} else {
$("#" + nm + "-error").removeClass("hidden").show().text(val);
}
});
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#update-email-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
}
} else if ( $(evt.target).attr("id") == "send-email") {
$.ajax(window.location.href, {
method: "POST",
data: {
action: $(evt.target).attr("id"),
},
})
.done(function(data) {
$('#modal-spinner').modal('hide');
if (data.Success) {
var msg = "Successfully sent test email.<br/>";
$("#send-email-result").removeClass("hidden").removeClass("error").show().html(msg).addClass("success");
} else {
jQuery.each(data.Errors, function(nm, val) {
nm = nm.toLowerCase();
if (nm == "emailsend") {
$("#send-email-result").removeClass("hidden").removeClass("success").show().html(val + "<br/>").addClass("error");
} else {
$("#" + nm + "-error").removeClass("hidden").show().text(val);
}
});
}
})
.fail(function(xhr, status, err) {
$('#modal-spinner').modal('hide');
$("#send-email-result").removeClass("hidden").removeClass("success").show().html(err + "<br/>").addClass("error");
});
} else if ( $(evt.target).attr("id") == "version-check") {
$.ajax(window.location.href, {
method: "POST",
@@ -1547,21 +1416,6 @@
check_fqdn();
$("#email-cb").change(function() {
$("#server").prop("required", $(this).prop("checked"));
$("#server").prop("disabled", !$(this).prop("checked"));
$("#port").prop("required", $(this).prop("checked"));
$("#port").prop("disabled", !$(this).prop("checked"));
$("#emailuser").prop("required", $(this).prop("checked"));
$("#emailuser").prop("disabled", !$(this).prop("checked"));
$("#emailpwd").prop("required", $(this).prop("checked"));
$("#emailpwd").prop("disabled", !$(this).prop("checked"));
$("#from").prop("required", $(this).prop("checked"));
$("#from").prop("disabled", !$(this).prop("checked"));
});
$("#email-cb").change();
$(".account-input").keyup(function() {
ok = $("#username").val() && $("#accemail").val() && $("#password").val();
$("#update-account").prop('disabled', !ok);