Open Source Contribution: This project is community-driven and Open Source! 🚀
If you spot a bug or have an idea for a cool enhancement, your contributions are more than welcome. Feel free to open an Issue or submit a Pull Request.
This project is licensed under the MIT License.
- Summary
- Project Overview
- What The Application Does
- Logic In
srv/server.js - Most Important Runtime Logic
- Important Technical Constraints
- Let's Encrypt Rate Limits and Test Mode
- Prerequisites
- Manual Steps Before Productive Use
- How To Use The Application
- Option 1: Productive usage on SAP BTP
- Option 2: Local hybrid development
- Useful Commands
- Project Structure
This application is used for fully automated certificate management with Let's Encrypt on SAP BTP Cloud Foundry.
App Overview
Certificate Details and Actions
Manage BTP scheduler job to automatically renew certificates
BTP Scheduler Jobs
It combines a CAP backend, a Fiori Elements UI, the SAP Custom Domain service, Cloud Foundry route management, and SAP Job Scheduler to support the full lifecycle of shared-domain certificates:
- create or recreate a CSR
- request a certificate from Let's Encrypt using the HTTP-01 challenge
- upload the certificate chain to SAP Custom Domain service
- activate the certificate for a custom domain
- renew active certificates automatically on a schedule
- delete expired certificates after a valid replacement exists
The main goal is to reduce manual certificate handling for custom domains hosted on SAP BTP while keeping the operational flow transparent in a UI.
Reference SAP documentation: What Is Custom Domain?
The key trick behind the automation is the use of SAP BTP Cloud Foundry path-based routing for the ACME challenge. Instead of requiring the target business application itself to serve the Let's Encrypt validation file, this project creates a dedicated BTP route for the path /.well-known/acme-challenge/ and points that path to this certificate application. Because of that, the ACME challenge can be answered here even when the actual productive application for the same custom domain is running in a different BTP space or even a different organization. In practice, the domain stays on the target application, while only the ACME challenge path is routed to this automation service.
Example domain: shop.example.com
shop.example.com
|
v
+-----------------------------+
| SAP BTP route configuration |
| for the same custom domain |
+-----------------------------+
| |
| |
normal traffic | | ACME challenge only
/* | | /.well-known/acme-challenge/*
v v
+---------------------------+ +------------------------------+
| Business application | | Certificate automation app |
| another BTP space or org | | this project |
| serves the actual app | | serves the ACME token file |
+---------------------------+ +------------------------------+
Result:
- users still reach the productive business application
- Let's Encrypt validation is answered by this certificate app
- only the ACME path is rerouted, not the whole domain
This application supports SAP BTP Extension Landscapes. However, because each landscape has its own dedicated load balancer, a separate instance of this certificate automation application must be deployed to each region.
The reason is technical: a DNS CNAME record for a custom domain points to a specific BTP load balancer. Since there is no shared load balancer across landscapes but instead one load balancer per landscape, each landscape requires its own certificate application instance to serve ACME challenges for domains in that region.
Reference: Custom Domains in Extension Landscapes
Deployment implication: If your organization uses multiple Extension Landscapes (e.g., eu10, eu10-004, us10, etc.), deploy this application once per landscape so that ACME challenges can be served locally in each region.
This project consists of three main parts:
srv/: CAP service implementation with the certificate, route, Cloud Foundry, and scheduler logicapp/certificates/: Fiori Elements UI for certificate operations and scheduler administrationmta.yaml: deployment descriptor for SAP BTP Cloud Foundry including required managed services
Important implementation details:
- The CAP entities in
db/data-model.cdsare virtual (@cds.persistence.skip). The application does not manage certificates in a local database. Instead, it reads and controls certificates through external APIs. - The UI is an OData V4 Fiori Elements app bound to
/catalog/. - Productive automation depends on service bindings to SAP BTP managed services, not on local mocks.
The central service is CatalogService in srv/cat-service.cds and srv/cat-service.js.
It exposes actions and functions for:
- reading available certificates, routes, domains, statuses, and environments
- creating a new key and CSR for a domain
- recreating a CSR from an existing certificate alias
- requesting a Let's Encrypt certificate in staging or production mode
- validating and uploading the certificate chain to SAP Custom Domain service
- activating and deactivating server certificates
- creating and checking the public HTTP-01 challenge route in Cloud Foundry
- creating, enabling, disabling, checking, and deleting Job Scheduler jobs
- renewing all active certificates automatically
- deleting obsolete expired certificates automatically
The implementation talks to several external APIs:
- SAP Custom Domain service API for keys, CSRs, server certificates, domains, and TLS configuration
- SAP Destination service to retrieve the technical destination for the Cloud Foundry API
- Cloud Foundry API to create and bind the ACME challenge route
- SAP Job Scheduler API to create and operate weekly background jobs
- Let's Encrypt via
acme-clientto complete the ACME flow
srv/server.js is intentionally small, but it is critical for the certificate flow.
On CAP bootstrap, it registers a static Express middleware for srv/static before the regular CAP middleware stack. That makes the path /.well-known/acme-challenge/ publicly reachable and not protected by XSUAA.
Why this matters:
- Let's Encrypt must be able to fetch the HTTP-01 challenge file anonymously
- the challenge files are written dynamically by the CAP service into
srv/static/.well-known/acme-challenge/ - if this path were protected by authentication, certificate issuance would fail
In short, srv/server.js provides the public ACME challenge endpoint that the rest of the automation depends on.
The main business logic is implemented in srv/cat-service.js.
For a new or existing domain, the service can create a key and CSR via SAP Custom Domain service.
Then it:
- selects the Let's Encrypt environment: staging (
T) or production (P) - verifies that the public challenge route is reachable
- requests a certificate with
acme-client - writes the ACME token file during
challengeCreateFn - removes the challenge file again during
challengeRemoveFn - splits the returned certificate chain
- validates the certificate chain with SAP Custom Domain service
- uploads the certificate chain
- activates the certificate in production mode
The application can verify or create the HTTP route needed for the ACME challenge.
When createRoute is executed, the service:
- resolves the current SAP BTP landscape from the Custom Domain service
- authenticates against the Cloud Foundry API using a technical user stored in Destination service
- creates a route with the path
/.well-known/acme-challenge/ - binds that route to the
letsencrypt-srvapplication
This route is what allows Let's Encrypt to reach the challenge file.
The function renewCertificates reads all active certificates and renews them one by one.
For each certificate it:
- generates a new alias with a timestamp
- creates a new CSR
- requests a new Let's Encrypt certificate
- uploads the chain
- activates the new certificate
When the request comes from SAP Job Scheduler, the actual work is moved into a background task because the scheduler expects a fast acknowledgment and has a timeout limit.
The function deleteExpiredCertificates removes expired certificates only when a valid active replacement for the same domain already exists.
This prevents accidental deletion of the only usable certificate for a domain.
The UI offers actions to:
- check whether the scheduler jobs exist and whether the weekly schedule is active
- create the jobs
- enable or disable the schedules
- delete the jobs again
The application manages two jobs:
Renew Active CertificatesDelete Expired Certificates
- Certificate issuance cannot be performed on
localhost. The service explicitly rejects ACME checks on local hosts. - The HTTP-01 challenge requires a publicly reachable domain and route.
- Productive certificate upload and activation are only done in production mode (
P). In staging mode the app stops after validation. - The scheduler jobs call the service endpoints directly. For this, the runtime must be able to determine a productive base URL.
- The app depends on external SAP BTP services and valid service bindings. It is not a standalone local-only application.
Let's Encrypt enforces strict rate limits. This is important for this project because repeated onboarding or troubleshooting on production can quickly consume issuance capacity.
Key limits (production, as documented by Let's Encrypt):
- New orders per account: up to 300 every 3 hours
- New certificates per registered domain: up to 50 every 7 days
- New certificates per exact same set of identifiers: up to 5 every 7 days
- Authorization failures per identifier per account: up to 5 per hour
Important operational notes:
- Rate limits are not manually resettable by revoking certificates.
- If a limit is hit, you must wait for refill (
Retry-After/ retry time in error response). - For development and troubleshooting, Let's Encrypt recommends using the staging environment.
To protect production rate-limit budget, this application supports a dedicated test/staging path:
- In certificate creation, use code
Tto run against Let's Encrypt staging instead of production (P). - For scheduled maintenance testing, the exposed functions allow test execution flags:
-
renewCertificates(test=true)-deleteExpiredCertificates(test=true)
Recommended practice:
- Run onboarding and troubleshooting with staging/test first.
- Only switch to production mode (
P) after route reachability and ACME challenge flow are verified.
References:
- Node.js 24.x as defined in
package.json - npm
- Cloud Foundry CLI (
cf) - MBT (
mbt) for MTA builds - access to an SAP BTP Cloud Foundry subaccount and space
The deployment descriptor creates or expects these managed services:
- XSUAA (
letsencrypt-auth) - SAP Custom Domain service (
custom-domain-service, serviceINFRA, plancustom_domains) - Destination service (
letsencrypt-destination) - SAP Job Scheduler (
letsencrypt-scheduler) - Alert Notification (
letsencrypt-alerting)
The destination cf_api is required to call the Cloud Foundry API. It must contain a technical user that can manage routes in the target subaccount and space.
The comments in srv/cat-service.js and the destination definition in mta.yaml imply these manual requirements for that technical user:
- create the user in IAS
- add the user to the Cloud Foundry subaccount user list
- add the user as organization member
- add the user as space developer
- maintain its username, password, and IAS origin in the destination configuration
The destination configuration in mta.yaml still contains placeholders and must be completed before productive use:
UserPasswordlogin_hint/ IAS origin
If alerting should be used, the comment in mta.yaml indicates an additional manual step:
- add
sap_cp_eu10_ans@sap.comasAuditorin the Cloud Foundry space
According to SAP Custom Domain service documentation, before configuring custom domains you must:
1. Acquire domain names
- Purchase one or more domain names from a domain registrar
- You own the domain, not SAP BTP
- Examples:
production.example.com,test.example.com,*.example.com(wildcard)
2. Obtain TLS/SSL certificates
- Get certificates from a Certificate Authority (CA)
- One certificate can cover a domain and its subdomains
- Certificates are owned by you, not by SAP BTP
- Example CAs: Let's Encrypt (free), DigiCert, GlobalSign, etc.
- This application uses Let's Encrypt via ACME
3. Manage the certificate lifecycle
- Critical: SAP BTP and SAP Custom Domain service do NOT provide automatic warnings if your certificates are about to expire
- You must use a certificate lifecycle management tool to monitor expiration
- This is what this Let's Encrypt application does automatically
- Expired certificates will break your custom domain connectivity without warning
4. Cloud Foundry permissions
- Org Manager: Required to manage private domains in Cloud Foundry using CF CLI
- Space Developer: Required to create Custom Domain service instances and manage certificates
- Best practice: create a separate space (e.g.,
custom-domains) for domain management
5. Create a Custom Domain service instance
- Use CF CLI or SAP BTP Cockpit
- Best practice: create only one instance per space
6. Configure TLS settings for your domain
- TLS configurations define SSL/TLS protocol versions, cipher suites, HTTP/2 support, and mTLS mode
- Create a
"default"TLS configuration using the SAP Custom Domain UI or CLI - Example properties: min TLS version 1.2, max TLS version 1.3, HTTP/2 support enabled
- Alert: The alert-config.json monitors for deprecated cipher suites via
TLSConfigurationCipherDeprecationevents
7. DNS CNAME configuration
- For each custom domain, create a DNS
CNAMErecord at your DNS provider. - Point the record to the SAP BTP load balancer host shown by the Custom Domain service for the corresponding landscape.
- In multi-landscape setups, create separate DNS records per landscape because each landscape has its own load balancer.
- Ensure the ACME challenge path is routable for the final host, for example:
https://<your-domain>/.well-known/acme-challenge/test. - Wait for DNS propagation before running productive certificate creation and activation.
- If you need to expose the apex/root domain, use your DNS provider's
ALIAS/ANAMEequivalent ifCNAMEis not allowed at zone root.
References:
The application automates certificate operations, but the landscape still requires initial manual setup.
- complete the destination service configuration for
cf_api - ensure Custom Domain service is available and bound
- ensure Job Scheduler is created with XSUAA support enabled
- ensure Alert Notification is configured if operational alerts are required
Useful commands from package.json:
npm install
npm run build
npm run deployOr in one step:
npm run buildDeployAfter deployment, open the approuter URL and launch:
/app/certificates/webapp/index.html
The UI uses XSUAA authentication.
For a new domain, the initial setup is not fully automatic. An operator typically needs to:
- create or verify the reserved domain
- create or verify the custom domain
- create or verify the ACME challenge route
- create a CSR or recreate it from an existing certificate
- run certificate creation in staging first if desired
- run productive certificate creation
- activate the certificate
Once a domain is onboarded and has an active certificate, recurring renewal can be automated.
The scheduler jobs are not created automatically by deployment. They must be created through the provided application action:
createRenewCertificatesScheduler
After that, the jobs can be checked, enabled, disabled, or deleted from the UI.
This is the intended operating mode.
- Deploy the MTA to Cloud Foundry.
- Verify all service bindings.
- Complete the destination configuration for the Cloud Foundry API technical user.
- Open the Fiori application.
- For each new domain, create the route and the certificate.
- Activate the certificate.
- Create the weekly scheduler jobs.
- Let the scheduler handle renewals and cleanup.
Local development is useful for UI and service testing, but not for full productive ACME issuance.
Available helper scripts:
npm run bindCustomDomainService
npm run bindDestinationService
npm run bindJobSchedulingService
npm run cfEnvToDefault
npm run cdsWatchHybridWhat these scripts are for:
bindCustomDomainService: bind the SAP Custom Domain service for hybrid CAP runsbindDestinationService: bind the Destination service for CF API accessbindJobSchedulingService: bind the Job Scheduler service for local scheduler testingcfEnvToDefault: extractVCAP_APPLICATIONfrom the deployed CF app intodefault-env.jsoncdsWatchHybrid: start the CAP app in hybrid mode with mocked authentication and open the certificates UI
Important notes for local use:
- mocked auth defines user
alicewith rolejobscheduler - local execution can be used to inspect the UI and test service integration
- actual Let's Encrypt certificate issuance still requires a public domain and reachable challenge route
renewCertificates(test=true)anddeleteExpiredCertificates(test=true)can be used for controlled tests
npm run login
npm run login004
npm run watch-certificates
npm run cdsWatchHybrid
npm run build
npm run deploy
npm run logSrv| Path | Purpose |
|---|---|
app/certificates/ |
Fiori Elements UI for certificate operations |
db/data-model.cds |
Virtual domain model exposed by CAP |
srv/cat-service.cds |
OData service definition |
srv/cat-service.js |
Certificate, route, CF API, and scheduler logic |
srv/server.js |
Public static ACME challenge endpoint |
srv/static/ |
Generated ACME challenge files |
mta.yaml |
Cloud Foundry deployment descriptor |
xs-security.json |
XSUAA configuration |
alert-config.json |
Alert Notification subscriptions |
default-env.json |
Extracted VCAP application data for local runs |






