GOOD NEW. The desired task is successfully and completely solved!
Because I see big interest - many views of this discussion - I decided to deal how I have implemented this task.
Let’s remember the task:
-
TRB140 (or any other similar device) shell receive HTTPS POST request from external source trough TRB140 WAN port, process it and send SMS message to the phones included in a POST request. SMS message shell be inside the POST request too.
-
Communication with user defined server inside TRB140, also WebUI and CLI must use SSL certificate from trusted provider (letsEncrypt for example) with automatic renewal.
By default from factory inside TRB140 are installed Teltonika self-signed certificates without automatic renewal. Teltonika is not a trusted provider. Therefore, when WebUi or CLI is accessed remotely through WAN port, browsers still use HTTPS (SSL) communication, however with red notification “Not secure”. CURL request from servers operate OK without any error messages.
The first part of task without encrypted SSL/TSL using HTTP POST request was implemented and tested very quickly and easily using Python HTTPServer and BaseHTTPRequestHandler from Python standard http module. The only challenge was compatibility between Python versions, because Teltonika offers for instalattion an old 3.9 version.
As far as concerns the second part of task - there was really big challenge.
Because TRB140 operating system is Linux based, for SSL certificates was made decision to use asme.sh bash script as it is simple and small.
Let’s for clarification at first parse the Vilius from Teltonika last post with advises for solution.
Key Points
…
2. Options for TRB140:
• Run acme.sh directly on the router: Possible, but fragile due to limited storage, firmware quirks, and cron reliability.
What does it mean limited storage in this situation? This is meaningless, because installed asme.sh occupies just several percent of free storage.
What does it mean firmware quirks? Of course firmware is not powerful and implementation of task shell be tested - however if tests is OK it means the firmware is operational.
What does it mean cron reliability? Crontab records can be tested too. Using automatic renewal there is only one simple crontab record and it should work. If Teltonika produces not reliable product, this product for serious application can not be used at all. Isn’t it?
• Run ACME client externally (recommended): Use your existing ACME setup on a server or PC (acme.sh, Certbot, win-acme) and push renewed certificates to the router automatically.
Absolutely inaccessible advice. TRB140 operating would be dependent on the external devices. If external devices would fail, TRB140 can fail too. TRB140 or similar device should insure independent operating at any time.
Recommended Approach (External Automation)
1. Issue/renew certificate externally using your usual ACME client:
acme.sh --issue --dns … -d device.yourdomain.com
This so called dns method is completely manual, because for verification it requires to add TXT type DNS record with randomly generated text from certificate provider and after certificate is issued to remove it. This procedure is manual and shell be done on first certificate issue and on subsequent renewals.
Other methods such as webroot or standalone require access to domains root directory www for writing. TRB140 /www directory is firmware one and therefore read only, so methods wouldn’t work at all.
2. Deploy to TRB140 automatically :
scp fullchain.pem root@TRB140_IP:/etc/uhttpd.crt
scp privkey.pem root@TRB140_IP:/etc/uhttpd.key
ssh root@TRB140_IP “/etc/init.d/uhttpd restart”
This is Unix/Linux bash commands. What to do if using other systems - Windows, Android, etc.? And again - dependency from external devices!
3. Full automation: acme.sh can run the above deploy/reload commands automatically after each renewal.
Why this method is best
Works reliably behind LTE/CGNAT networks
Avoids router storage and package limitations
Fully automated renewal with zero manual steps
Uses trusted certificates, eliminating browser security warnings
Of course it’s clear now that this “best method” it is not a truth .
MY IMPLEMENTATION OF API FOR THE MENTIONED TASK
New key points
Using asme.sh bash script here still is one method so called DNS-API suitable for automatic renewal.
Verification of this method is also based on creating DNS TXT records, however DNS provider mast have implemented API with automatic appending and removing of such record.
In Lithuania no one provider has such API.
Below is a worldwide list of other DNS providers which support DNS-API method:
Task was implemented by this method choosing Cloudflare DNS provider.
Step by step explanation
1. DNS records concerning TRB140 from local provide I have exported to Cloudflare.
2. From GitHub to Windows PC was downloaded acme.sh zipped package and unzipped.
3. In root@TRB140:/tmp/#directory was created temporary directory: root@TRB140:/tmp/# mkdir acme.
4. Using Windows WinSCP acme package was copied to root@TRB140:/tmp/acme# directory.
5. In the root@TRB140:/etc# directory was created root@TRB140:/etc# mkdir acme directory for installing acme.sh.
6. From root@TRB140:/etc/acme# directory was made acme.sh installation:
root@TRB140:/etc/acme# acme.sh --home /etc/acme --accountemail "<your email for acme messaging>"
--home /etc/acme: Sets the installation and configuration directory.
--accountemail: Required for registration with the Certificate Authority (like Let’s Encrypt).
7. From root@TRB140:/tmp# removed temporary root@TRB140:/tmp# rm -rf acme directory.
8. Changed directory to root@TRB140:/etc/acme#
9. Made acme.sh executable: root@TRB140:/etc/acme# chmod +x /etc/acme/acme.sh
10. To use the command simply typing as acme.sh from anywhere to file root@TRB140:/etc/profile should be added alias
root@TRB140:/etc/acme# echo "alias acme.sh='/etc/acme/acme.sh'" >> /etc/profile
root@TRB140:/etc/acme# source /etc/profile
because profile file is executed on TRB140 rebooting so it ensures keeping alias persistent.
11. For using Cloudflair DNS API the acme.sh (or rather LetsEncrypt) must know how to access this API.
For that using internal vi editor to root@TRB140:/etc/profile file manually by root@TRB140:/etc# vi profile must be added persistent variables:
...
export ENV=/etc/shinit #existing one
export CF_Key="47f9ce558acda0a54559678c60e1e007c4cac" #new added
export CF_Email="<email in Claudflair account>" #new added
case "$TERM" in #existing one
...
Please notice that in the internet you can find following advice to execute bash commands:
bash
export CF_Key="your_global_api_key"
export CF_Email="your email in Claudflair account"
In our case it is wrong, because a life time of variables is until rebooting. After TRB140 rebooting these variables will no longer exist.
So for persistence variables must be included in the root@TRB140:/etc/profile file.
Here:
CF_Keyis aGlobal API Key value copied from Cloudflair account profile>API Tokens.
Do not confuse with Origin CA Key - must be Global API Key!
CF_Emailis email from Cloudflair accountprofile>Settings.
12. For certificate generation mast be used following bash command:
root@TRB140:/...#acme.sh --issue --dns <dns_provider_name> -d <TRB140 dns name>
<dns_provider_name> for Cloudflair is dns_cf., so replace <dns_provider_name> with dns-cf.
13. Check if in the cron job for automatic renewal is created:
root@TRB140:/...#crontab -l
On TRB140 CLI you have to see the bash response:
59 7 * * * "/etc/acme"/acme.sh --cron --home "/etc/acme" > /dev/null
This string is added automatically by acme.ch to file root@TRB140:/etc/crontabs/root .
You can include this string manually too: root@TRB140:/…# crontab -e .
This bash command invokes vi editor of root file.
14. Now you shall replace self-signed Teltonika files with already generated from trusted DNS provider.
However before this action it is recommended to backup Teltonika files:
cp /etc/uhttpd.crt /etc/uhttpd.crt.bak
cp /etc/uhttpd.key /etc/uhttpd.key.bak
Replacing files by bash command:
root@TRB140:/...#acme.sh --install-cert -d <TRB140 domain name> \
--cert-file /etc/uhttpd.crt \
--key-file /etc/uhttpd.key \
--fullchain-file /etc/uhttpd.crt \
--reloadcmd "/etc/init.d/uhttpd restart"
Is it all? Not. Now TRB140 WebUI will be accessible by domain name, however command line interface CLI not accessible jet. Why?
When in Cloudflair A type DNS records was imported or created, Cloudflair proxy server is enabled automatically. So all traffic goes trough proxy:
[Internet client]↔[Cloudflair proxy]↔[TRB140] .
However Cloudflair proxy does not support SSH on 22 port, which is required for communication with CLI.
So you need again to login in CloudFlair and disable proxy. After disabling proxy communication will be direct and CLI become accessible:
[Internet client]↔[TRB140] .
Cons and pros of direct communication
Cons. Using domain access TRB140 WAN IP address becomes visible throughout the world.
Pros. TRB140 inside is proof from attackers, because WebUI and CLI server root directory /www belongs to so called read-only file system (firmware). However from intensive automatically generated request attacks TRB140 is not protected.
Python server for POST request reception and SMS sending
At first for communication it should be assigned free port, because SSL port 443 is used by TRB140 WebUI. Let’s say it can be 8443.
So request URL should be https://<TRB 140 domain name>:8443. Using non typical port also protects from majority of attacks, because almost all attackers use standard ports.
Using POST requests only also has protective effect, because they can be send from client side JavaScript or CURL requests from servers only. From address bar of browsers POST requests can’t be send.
Post request from client JavaScript:
'use strict';
const array[<your array structure with SMS messages and phone numbers>],
Body=JSON.stringify(array),
URL="<TRB140 domen name>:8443",
Post=async()=>{
<your code here>
let response=await fetch(URL,{method:'POST',headers:{'Accept':'application/json','Content-Type':'application/json'},body:Body});
if(response.ok){<your code here>}
<your code here>
};
Post();
Python HTTPS server on TRB140 (.py file):
from http.server import BaseHTTPRequestHandler,HTTPServer
from urllib.parse import urlsplit,parse_qsl
import logging,json,os,ssl
class S(BaseHTTPRequestHandler):
def _set_response(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin','*')
self.end_headers()
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin','*')
self.send_header('Access-Control-Allow-Headers','Content-Type')
self.end_headers()
def log_request(self, code='-', size='-'):
if code != 200:
super().log_request(code, size)
def do_POST(self):
p_data=self.rfile.read(int(self.headers['Content-Length']))
self._set_response()
v1=json.loads(p_data)
for x1 in v1:
for x2 in x1[1]:
os.system('gsmctl -S -s "'+x2+x1[0]+'" > /dev/null 2>&1')
self.wfile.write(b'')
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
cert_path = '/etc/uhttpd.crt'
key_path = '/etc/uhttpd.key'
context.load_cert_chain(certfile=cert_path, keyfile=key_path)
httpd=HTTPServer(('<TRB140 WAN IP>',8443),S)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()
For normal without errors POST requests from browsers (tested with several most popular browsers) the following conditions should be met:
- Content security policy CSP must be set to:
“default-src ‘self’ https://:8443”.
- As you can see in the Python code above:
2.1. In the function _set_response(self) must be included self.send_header(‘Access-Control-Allow-Origin’, ‘*’) ;
2.2. Must be included function:
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin','*')
self.send_header('Access-Control-Allow-Headers','Content-Type')
self.end_headers()
2.3. In the function do_POST(self) must be included self.wfile.write(b’') , which sends empty feedback to the browser.
Without fulfilling all mentioned conditions browser gives errors and not work.
The main disadvantage is still security, because everyone can implement direct access from browsers.
So much better is making request from single proxy server using CURL POST requests.
All clients shell send POST requests to the proxy.
On a TRB140 can be set firewall rule to accept requests only from single proxy WAN IP. So for all attackers direct access will be impossible.
Also all mentioned conditions required for direct accessing from client is not required for accessing from proxy.
Python server code becomes much simple and not accessible directly from client.
from http.server import BaseHTTPRequestHandler,HTTPServer
from urllib.parse import urlsplit,parse_qsl
import logging,json,os,ssl
class S(BaseHTTPRequestHandler):
def _set_response(self):
self.send_response(200)
self.flush_headers()
def log_request(self, code='-', size='-'):
if code != 200:
super().log_request(code, size)
def do_POST(self):
p_data=self.rfile.read(int(self.headers['Content-Length']))
self._set_response()
v1=json.loads(p_data)
for x1 in v1:
for x2 in x1[1]:
os.system('gsmctl -S -s "'+x2+x1[0]+'" > /dev/null 2>&1')
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
cert_path = '/etc/uhttpd.crt'
key_path = '/etc/uhttpd.key'
context.load_cert_chain(certfile=cert_path, keyfile=key_path)
httpd=HTTPServer(('<TRB140 WAN IP>',8443),S)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()
Please notice that in the function _set_response(self) is used self.flush_headers() instead of self.end_headers() .
This works normally from proxy, however do not work from browsers - again better for security.
Below is a PHP code of proxy server on Apache platform:
<?php
if($_SERVER['REQUEST_METHOD']!=='POST'){http_response_code(403);exit('ERROR 403: PROHIBITED!');}
$payload=file_get_contents('php://input');
$data=null;$ch=curl_init();
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,['Content-Type:application/json']);
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_URL,'https://<TRB140 domain>:8443');
curl_setopt($ch,CURLOPT_POSTFIELDS,$payload);
curl_exec($ch);
$errstr=null;
if(curl_errno($ch)){$errstr=curl_errno($ch).": ".curl_error($ch);}
if(isset($errstr))exit($errstr);
echo "OK";
?>
Notice that this code accepts only POST requests. For other requests code returns: ‘ERROR 403: PROHIBITED!’. This is security element too.
In this way we have communication traffic:
[Internet clients]↔[Proxy server]↔[TRB140 Python server]
As an example a JavaScript code of an internet client:
'use strict';
const
Post=async(rq)=>{
try{let r=await fetch(rq[0],{method:'POST',headers:{'Content-Type':'application/json'},body:rq[1]})
if(r.ok){
r=await r.text();
document.body.textContent=r; //or any other your code
}else console.log(r.status);
}
catch(err){console.log(err)}
},
arr1=[<your array structure with SMS messages and phone numbers>];
Post(['<Proxy server URL>',JSON.stringify(arr1)]);
The implemented task with this API was tested with many SMS messages to single phone and many SMS messages to many phones.
Everything operates OK with no errors.
If required the communication can be additionally protected by one or double authentications: passwords (or 2FA, or others methods) from client to proxy and from proxy to TRB140 python server.
I HOPE THIS POST WILL HELP FOR SOMEBODY