1
0
mirror of https://github.com/mainflux/mainflux.git synced 2025-04-26 13:48:53 +08:00

MF-651 - X509 Mutual TLS authentication (#676)

* Use NginX njs module for mutual authentication

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add Makefile for cert management

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Move certificates make context to scripts dir

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Move nginx.conf to separate directory

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Choose between two NginX configurations

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Move certs Makefile to docker/ssl/

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Use default key-based authentication

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add mTLS docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update Makefile

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add check if Authorization is present

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add check if Will Flag is 1

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Return MQTT over WS

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Fix docker-compose.yml volume mapping

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Rename security section in docs

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add message type check before message parsing

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove double comments

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove s.AGAIN in return

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Update Makefile

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Remove CSR and key from the root

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Drop TLS version below 1.2

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>

* Add comments for cert and key paths

Signed-off-by: Dušan Borovčanin <dusan.borovcanin@mainflux.com>
This commit is contained in:
Dušan Borovčanin 2019-04-02 17:54:24 +02:00 committed by Drasko DRASKOVIC
parent bc49f6a543
commit f9b17d5f24
16 changed files with 830 additions and 194 deletions

View File

@ -14,12 +14,14 @@ networks:
services:
nginx:
image: nginx:1.14.2-alpine
image: nginx:1.14.2
container_name: mainflux-nginx
restart: on-failure
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./nginx/nginx-${AUTH-key}.conf:/etc/nginx/nginx.conf
- ./ssl/authorization.js:/etc/nginx/authorization.js
- ./ssl/certs/mainflux-server.crt:/etc/ssl/certs/mainflux-server.crt
- ./ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
- ./ssl/certs/mainflux-server.key:/etc/ssl/private/mainflux-server.key
- ./ssl/dhparam.pem:/etc/ssl/certs/dhparam.pem
ports:

View File

@ -1,16 +1,11 @@
###
# Mainflux NGINX Conf
#
# Taken for /etc/nginx/nginx.conf on Debian machine
# and https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/nginx.conf
###
# Copyright (c) 2018
# Mainflux
#
# SPDX-License-Identifier: Apache-2.0
#
# This is the default Mainflux NGINX configuration.
##
# User:
# - 'www-data' on Debian
# - 'nginx' on Alpine
##
#user www-data;
user nginx;
worker_processes auto;
pid /run/nginx.pid;
@ -18,77 +13,37 @@ include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
###
# HTTP
###
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Virtual Host Configs
##
# HTTPS
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
# Certificates
# These paths are set to its default values as
# a volume in the docker/docker-compose.yml file.
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
@ -98,15 +53,12 @@ http {
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now. You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers "*";
add_header Access-Control-Allow-Headers '*';
server_name localhost;
@ -200,8 +152,8 @@ http {
return 200;
}
}
# Proxy pass to mainflux-mqtt-adapter
# Proxy pass to mainflux-mqtt-adapter over WS
location /mqtt {
proxy_redirect off;
proxy_set_header Host $host;
@ -244,21 +196,17 @@ http {
# MQTT
stream {
# MQTT
server {
listen 8883 ssl;
listen [::]:8883 ssl;
# Certificates
# These paths are set to its default values as
# a volume in the docker/docker-compose.yml file.
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;

View File

@ -0,0 +1,259 @@
#
# Copyright (c) 2018
# Mainflux
#
# SPDX-License-Identifier: Apache-2.0
#
# This is the Mainflux NGINX configuration for mututal authentication based on X.509 certifiactes.
user nginx;
worker_processes auto;
pid /run/nginx.pid;
load_module /etc/nginx/modules/ngx_stream_js_module.so;
load_module /etc/nginx/modules/ngx_http_js_module.so;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
js_include authorization.js;
js_set $auth_key setKey;
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
# These paths are set to its default values as
# a volume in the docker/docker-compose.yml file.
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_client_certificate /etc/ssl/certs/ca.crt;
ssl_verify_client optional;
ssl_verify_depth 2;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
ssl_stapling off;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers '*';
server_name localhost;
# Proxy pass to users service
location ~ ^/(users|tokens) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://users:8180;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to things service
location ~ ^/(things|channels) {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
location /version {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://things:8182;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-http-adapter
location /http/ {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
if ($auth_key = '') {
return 403;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $auth_key;
proxy_pass http://http-adapter:8185/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-ws-adapter
location /ws/ {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
if ($auth_key = '') {
return 403;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://ws-adapter:8186/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
# Proxy pass to mainflux-mqtt-adapter over WS
location /mqtt {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
if ($auth_key = '') {
return 403;
}
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
proxy_pass http://mqtt-adapter:8880/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://ui:3000/;
# Allow OPTIONS method CORS
if ($request_method = OPTIONS ) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
}
error_log info.log info;
error_log error.log error;
error_log warn.log warn;
}
# MQTT
stream {
js_include authorization.js;
server {
listen 8883 ssl;
listen [::]:8883 ssl;
# These paths are set to its default values as
# a volume in the docker/docker-compose.yml file.
ssl_certificate /etc/ssl/certs/mainflux-server.crt;
ssl_certificate_key /etc/ssl/private/mainflux-server.key;
ssl_client_certificate /etc/ssl/certs/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
js_preread authenticate;
proxy_pass mqtt-adapter:1883;
}
}
error_log info.log info;
error_log error.log error;
error_log warn.log warn;

42
docker/ssl/Makefile Normal file
View File

@ -0,0 +1,42 @@
CRT_LOCATION = certs
KEY = default
O = Mainflux
OU = mainflux
EA = info@mainflux.com
CN = localhost
CRT_FILE_NAME = thing
all: clean_certs ca server_crt
# CA name and key is "ca".
ca:
openssl req -newkey rsa:2048 -x509 -nodes -sha512 -days 1095 \
-keyout $(CRT_LOCATION)/ca.key -out $(CRT_LOCATION)/ca.crt -subj "/CN=localhost/O=Mainflux/OU=IoT/emailAddress=info@mainflux.com"
# Server cert and key name is "mainflux-server".
server_cert:
# Create mainflux server key and CSR.
openssl req -new -sha256 -newkey rsa:4096 -nodes -keyout $(CRT_LOCATION)/mainflux-server.key \
-out $(CRT_LOCATION)/mainflux-server.csr -subj "/CN=$(CN)/O=$(O)/OU=$(OU)/emailAddress=$(EA)"
# Sign server CSR.
openssl x509 -req -days 1000 -in $(CRT_LOCATION)/mainflux-server.csr -CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial -out $(CRT_LOCATION)/mainflux-server.crt
# Remove CSR.
rm $(CRT_LOCATION)/mainflux-server.csr
thing_cert:
# Create mainflux server key and CSR.
openssl req -new -sha256 -newkey rsa:4096 -nodes -keyout $(CRT_LOCATION)/$(CRT_FILE_NAME).key \
-out $(CRT_LOCATION)/$(CRT_FILE_NAME).csr -subj "/CN=$(KEY)/O=$(O)/OU=$(OU)/emailAddress=$(EA)"
# Sign client CSR.
openssl x509 -req -days 730 -in $(CRT_LOCATION)/$(CRT_FILE_NAME).csr -CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial -out $(CRT_LOCATION)/$(CRT_FILE_NAME).crt
# Remove CSR.
rm $(CRT_LOCATION)/$(CRT_FILE_NAME).csr
clean_certs:
rm -r $(CRT_LOCATION)/*.crt
rm -r $(CRT_LOCATION)/*.key
rm -r $(CRT_LOCATION)/*.srl

176
docker/ssl/authorization.js Normal file
View File

@ -0,0 +1,176 @@
var clientKey = '';
// Check certificate MQTTS.
function authenticate(s) {
if (!s.variables.ssl_client_s_dn || !s.variables.ssl_client_s_dn.length ||
!s.variables.ssl_client_verify || s.variables.ssl_client_verify != "SUCCESS") {
s.deny();
return
}
s.on('upload', function (data) {
if (data == '') {
return;
}
var packet_type_flags_byte = data.codePointAt(0);
// First MQTT packet contain message type and flags. CONNECT message type
// is encoded as 0001, and we're not interested in flags, so only values
// 0001xxxx (which is between 16 and 32) should be checked.
if (packet_type_flags_byte < 16 || packet_type_flags_byte >= 32) {
s.off('upload');
s.allow();
return;
}
if (clientKey === '') {
clientKey = parseCert(s.variables.ssl_client_s_dn, 'CN');
}
var pass = parsePackage(s, data);
if (!clientKey.length || pass !== clientKey) {
s.error('Cert CN (' + clientKey + ') does not match client password');
s.off('upload')
s.deny();
return;
}
s.off('upload');
s.allow();
})
}
function parsePackage(s, data) {
// An explanation of MQTT packet structure can be found here:
// https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#msg-format.
// CONNECT message is explained here:
// https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect.
/*
0 1 2 3
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| TYPE | RSRVD | REMAINING LEN | PROTOCOL NAME LEN |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PROTOCOL NAME |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
| VERSION | FLAGS | KEEP ALIVE |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
| Payload (if any) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
First byte with remaining length represents fixed header.
Remaining Length is the length of the variable header (10 bytes) plus the length of the Payload.
It is encoded in the manner described here:
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html#_Toc442180836.
Connect flags byte looks like this:
| 7 | 6 | 5 | 4 3 | 2 | 1 | 0 |
| Username Flag | Password Flag | Will Retain | Will QoS | Will Flag | Clean Session | Reserved |
The payload is determined by the flags and comes in this order:
1. Client ID (2 bytes length + ID value)
2. Will Topic (2 bytes length + Will Topic value) if Will Flag is 1.
3. Will Message (2 bytes length + Will Message value) if Will Flag is 1.
4. User Name (2 bytes length + User Name value) if User Name Flag is 1.
5. Password (2 bytes length + Password value) if Password Flag is 1.
This method extracts Password field.
*/
// Extract variable length header. It's 1-4 bytes. As long as continuation byte is
// 1, there are more bytes in this header. This algorithm is explained here:
// http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html#_Toc442180836
var len_size = 1;
for (var remaining_len = 1; remaining_len < 5; remaining_len++) {
if (data.codePointAt(remaining_len) > 128) {
len_size += 1;
continue;
}
break;
}
// CONTROL(1) + MSG_LEN(1-4) + PROTO_NAME_LEN(2) + PROTO_NAME(4) + PROTO_VERSION(1)
var flags_pos = 1 + len_size + 2 + 4 + 1;
var flags = data.codePointAt(flags_pos);
// If there are no username and password flags (11xxxxxx), return.
if (flags < 192) {
s.error('MQTT username or password not provided');
return '';
}
// FLAGS(1) + KEEP_ALIVE(2)
var shift = flags_pos + 1 + 2;
// Number of bytes to encode length.
var len_bytes_num = 2;
// If Wil Flag is present, Will Topic and Will Message need to be skipped as well.
var shift_flags = 196 <= flags ? 5 : 3;
var len_msb, len_lsb, len;
for (var i = 0; i < shift_flags; i++) {
len_msb = data.codePointAt(shift).toString(16);
len_lsb = data.codePointAt(shift + 1).toString(16);
len = calcLen(len_msb, len_lsb);
shift += len_bytes_num;
if (i != shift_flags - 1) {
shift += len;
}
}
var password = data.substring(shift, shift + len);
return password;
}
// Check certificate HTTPS and WSS.
function setKey(r) {
if (clientKey === '') {
clientKey = parseCert(r.variables.ssl_client_s_dn, 'CN');
}
var auth = r.headersIn['Authorization'];
if (auth.length && auth != clientKey) {
r.error('Authorization header does not match certificate');
return '';
}
if (r.uri.startsWith('/ws') && !auth.length) {
var a;
for (a in r.args) {
if (a == 'authorization' && r.args[a] === clientKey) {
return clientKey;
}
}
r.error('Authorization param does not match certificate');
return '';
}
return clientKey;
}
function calcLen(msb, lsb) {
if (lsb < 2) {
lsb = '0' + lsb;
}
return parseInt(msb + lsb, 16);
}
function parseCert(cert, key) {
if (cert.length) {
var pairs = cert.split(',');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
if (pair[0].toUpperCase() == key) {
return pair[1];
}
}
}
return '';
}

View File

@ -1,21 +1,22 @@
-----BEGIN CERTIFICATE-----
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
5Yq2
-----END CERTIFICATE-----

28
docker/ssl/certs/ca.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCq6O4PHwgGOmEa
fjea5KocG80GYSYbvN37ums6fQ1wcmCxn8LtZek8WkfJS2NQQPDvn8QWRY7aUkTA
W7cEB4vxpT25bevP7KJNFAS8XZO7NTfF8fscJS+YWSXzVS0OFZ2YuqTnjCiqWf5m
vjAkkXBGIYq+k2ONM1tHlEA0lzbLun2a9H/XarCG+znjpfYpW6R08zFzXyGb4sI2
pyYpP7iZLla7PTSZTt9h6jkY3qqMDhEHhPdlXDhO1O9/lA8yWMO9vKCzC7ngDXnV
99Nl+tFhp9z9VkTUveLMuN9+riDJRfP25fOzHuRYzmsRemYjD1NvSgsvFqSbFDVX
B8kcyrXPAgMBAAECggEAbp/el0MKup1HBRL1gvjHcvI7vwla1VFmje2YQn93F3Wx
SMeUMH1qfnohRRXa7rNaQIA1OAVF9eKSRcAXsjAAUSUX0tJndGpCk4mFlzcqzF4h
/6olU45uRDpP6jUTuK4dGCKXYpjCKaGenXo1RzYsafiECd707Qx05Nv8ww2tlifN
HtUR0xCZfVGDZfmNMZVrksUIZ1XHwZNtNLWQW6MBl3RhFaA0Wz/RfFMi2FzacEbj
75IqE6PLic1fin6P3GouzKamtZ6YPTyR5PqxCOCw97oZDCUGy2qGyAuPUi9O2HKB
fQgSyIxuR73S2korvxAmvekubjBFAqhan2oEjZs6oQKBgQDT28COlC33BSrpr2+V
pZIL4Bb1rGHreTi1M/4n9nP3GOZ9gqnSUsWXyxYVoZ2YfixorjZhUzHyx4SfZ2E9
p5PkIJ0wOiHLlKQ36vEVN9ZO1UyNCYUgs3seW40xnsAiMNczZjufIZrsejO3tc2j
Jhgp+B/9Bt5A8us2ewhz3LlQowKBgQDOhQmZAfL/xAjYBCUS73t/YO60i5e1yg2J
i6jXeKjd5gRZ32upkBzQ8UBvAGSQGqrcCnqIzrU5TeeD046bZzkokg7iKwHwQDrL
SXTthUB6ABZddP/VXCEUVBer3FEnUgJm9jw08RzmPyNEPjfp91FDmJ9GYcbdo/nL
hBPHh3lc5QKBgQCJYZ0yWACeiKlVNECFqAJW1Q/Oa+RrkAYn6vlK7NQyTeFZTlvV
WXtsfXNqv4y0kE037JCy+AIRzzO/MoiqNHsAme2Ukn3LyC3dXOrMuZKtOEAVzTCZ
Dgoum2up26n4AffrCsZq4J3X7z6OSMR6oX9V5+LGb6e8Mko43/uRNnatRQKBgEMH
bQkLV+ppnxE1ry7JKcU7Gd7hm9j1/pTRDnj5AZ4b5Peii1ganS+3zdj5QKqA7UnD
4Od8Z9d0kJr51EReKXAgj9IacWOgBTUr31akNDwkwR2ONubyIw5tCM3QEUr41CzE
6N+qDl4wyeqBYzZ9/hM5eyCl5ZzUduP2N1FAiER9AoGAW2T0OeM5ZsPABMKu9eEN
FB9bVysqWT1tExB34OGWrZvNEzsHTqvr/D3KSWv0PS1pM46M1XkVbybOzRmPrzab
AGMDJXgGhMuk2UtDA/s9mgqTOeDXpvmaFyThVkoH162j6GMuX2SwxHnH9D42zgMR
3LEZ/5Q5HMJ4jwEM880jvP4=
-----END PRIVATE KEY-----

1
docker/ssl/certs/ca.srl Normal file
View File

@ -0,0 +1 @@
27207EA9519D3D252E08AFA38D23BF2928FD5E20

View File

@ -1,29 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIFDDCCA/SgAwIBAgIJANUB7oWmhqk8MA0GCSqGSIb3DQEBDQUAMFcxEjAQBgNV
BAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsMA0lvVDEg
MB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTgxMTAyMTc1MzQ0
WhcNMzIxMDI5MTc1MzQ0WjBdMRgwFgYDVQQDDA9tYWluZmx1eC1zZXJ2ZXIxETAP
BgNVBAoMCE1haW5mbHV4MQwwCgYDVQQLDANJb1QxIDAeBgkqhkiG9w0BCQEWEWlu
Zm9AbWFpbmZsdXguY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
ovtHVZacXvf5m7iyhLdGqBaoTwUMSWR+LDaHyeUAcqLy4AHe2TGdJmbrsSKJOb94
Winru0dgQyBE1a/I+MOPYVNbYmShOoUSH6/a2Ph3DM8C7PjaFsc+uKd6NiVRmFid
c+3pQ62Um9mgJ5jD6kFB2uO5Bk9zxlDRGZz2BYvMFGcbhDZzf28O/Wwitfjb3dek
8kohknaIgHy50qstxt/GRFVHpqK0B+PVubC3Dr1Ext+lZORqM44o36jdUyVhnFXf
f8Fj/g4whrJfq3AOHeMsxm1VLqKeQb9CxpWvUi396w/bEXOuKXyoP5hyMUAvXEoE
pw2+M/CPHuCzj/ELafjWjQIDAQABo4IB0zCCAc8wDAYDVR0TAQH/BAIwADARBglg
hkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgXgMCoGCWCGSAGG+EIBDQQdFhtNYWlu
Zmx1eCBTZXJ2ZXIgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFDRwdZDGidKOpuJ29hXz
RWRFD8YkMIGIBgNVHSMEgYAwfoAUMmE/LCiFwPSKXDSAQvcRF/9yM56hW6RZMFcx
MIIEOjCCAyICFCcgfqlRnT0lLgivo40jvyko/V4fMA0GCSqGSIb3DQEBCwUAMFcx
EjAQBgNVBAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsM
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb22CCQCutSPtdZHO
JzA7BgNVHREENDAyhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABggV1c2Vyc4IGdGhp
bmdzgglsb2NhbGhvc3QwgYsGA1UdIASBgzCBgDB+BgMrBQgwdzAcBggrBgEFBQcC
ARYQaHR0cDovL2xvY2FsaG9zdDBXBggrBgEFBQcCAjBLMA8WCE1haW5mbHV4MAMC
AQEaOFRoaXMgQ0EgaXMgZm9yIGEgbG9jYWwgTWFpbmZsdXggc2VydmVyIGluc3Rh
bGxhdGlvbiBvbmx5MA0GCSqGSIb3DQEBDQUAA4IBAQAwrfzfLLe35aTGel9tWpWi
aWSATgYThCzaFqqzsQBNAmB/S+06xI2JSeXtHOestsqLZOrWdGG6LJcRiyCR/XWv
SxDRPUgjp14YQCml8GWQLairhdXNWZch1d+Bzr2XkJrTzQUex/XCJQnp56CzjFUO
XADhQdiaESvu/tk7nRuX8qYqwyIqwzRm5KlqHJIvsDXddGluS5EtsshdtAwbRQdR
jK1egJ3Z26vn68zrZiPQOYz9gmJs2Zl71bd4cGEBa6m7RGi9ww7gU6TsMoE8BTDg
i2ia+MB+COQB8ISx1+Pyx2migImZlnlYfSup1ObboyJV2jdqROBWbaosHqhh/7VF
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTkwNDAx
MDkyNzA1WhcNMjExMjI2MDkyNzA1WjBcMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAP
BgNVBAoMCE1haW5mbHV4MREwDwYDVQQLDAhtYWluZmx1eDEgMB4GCSqGSIb3DQEJ
ARYRaW5mb0BtYWluZmx1eC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQD1LayMnOYlTHWkK/7BIc2nRLkfkbfyejIujEKIuOPYZ1DbG36VeSM1MYlp
zl+E7gJvvK1RuCcL4DKG0uExI6HV2GdEq9kSe3Pj/512VTq+bXvMDRTcHMUkaN0N
J7GybHNk8J3fmFlB61zUpZUNy0M14YYX8tZRMMw8Ke6ThJyj8ulSky4Cp2tfiGK9
+YLP/UJkSm+0EOVAOMAtLNvXtg5+/0e63M+stdf+F3txLuiYXiOG399tXlI61r4L
5fKs0xau6P1V5uEPwAnQiXYVLCdahfGrUJIjHnHTU0TS2EpE8OxAu0krzQeONGSU
g6SMM8vCP0d8yqQrYZGkmaFmIiTgOmy/fs+8u/ykautiOR/SviTR3hi/ofjZ+NTd
T2Udg98BGuZBwKw+elajHUSUEkxtJVxeuFiVGzZNXkEhuxU6VNCnPeXxtl502rU9
nmhmO2WJ0/1KX+oe/uTC99b+olEPm72exsX0mwkSpIwDRBpX9meER4vJe4yX9fmo
tqEC2G30C9KYn+STcY9P7jptJgLLuN61DVBjeMPLW+0NTjqmtplcu73zYvyCsG4r
hIhY291wvz18iNLY7BfehU3beEx68ApdLMue6xi9JlFKxHf5FHBnBSvD2xrR47rH
9UMOHLglB+QkoidQ3KugHJ8r1sVHPhuS8mE7cENReFoNfh+N2wIDAQABMA0GCSqG
SIb3DQEBCwUAA4IBAQARH5ZD86TPaKW7Dty1bAnj1owp0o+DOp65hGZOZ2AqYVDF
UMz46ahAuBWhHPIiSkBnonBL5xVV3qihhlISaOQKe2FPdt/ekhUTzI/upAZDphN0
m4ZNllXaHAA0IQpXp3O/An6/IhrLCGLth9pnIzswi6sF+I5nIfpcuAV7TJfLUAG+
UTjy8GsZhE/ZCx0JSYzhpC1mDGxtyCQR7QY7rnEohXv0bHmv/jVVIZenT2SZZHJ5
sQEiaIZWbpHctpgbom1qi5BNmIz9APKus3f8ACGuMLOHiW1u6I8vl4b1kqc44Qoe
2c5uGEHh+Iv6v/V5JwzTrfbcaWeAv058NnN9rF8i
-----END CERTIFICATE-----

View File

@ -1,27 +1,52 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAovtHVZacXvf5m7iyhLdGqBaoTwUMSWR+LDaHyeUAcqLy4AHe
2TGdJmbrsSKJOb94Winru0dgQyBE1a/I+MOPYVNbYmShOoUSH6/a2Ph3DM8C7Pja
Fsc+uKd6NiVRmFidc+3pQ62Um9mgJ5jD6kFB2uO5Bk9zxlDRGZz2BYvMFGcbhDZz
f28O/Wwitfjb3dek8kohknaIgHy50qstxt/GRFVHpqK0B+PVubC3Dr1Ext+lZORq
M44o36jdUyVhnFXff8Fj/g4whrJfq3AOHeMsxm1VLqKeQb9CxpWvUi396w/bEXOu
KXyoP5hyMUAvXEoEpw2+M/CPHuCzj/ELafjWjQIDAQABAoIBACZM2KmjWCH1MkJG
+CS6ES+lC25uaEHDDOeE9Qob96v44QyFRAJUZ3LD0vVwgUEB3t2JZm0/S77akXKJ
+beD9WjQtvP7Y+wlo2mw4MQGN6vZ5f0cSdv6fKHWMaERygf8IxxAN4YA/6BUVw+X
FcsyBLDUvQFfoHxlN45GVYZeINpWNJEM5NkQpoz5JJmWplfRREtRDiFfdFvh8l25
nOeYaZkD9IAPW5k7ukzzUM4voNOKoutCV3h30AbD8zNMPblatLTrT9ZRtjEiofDn
iNT6DR2Ge4WWN8e+9h9CEklhhCwBULNlCg2mdNKsiU3q74oBExiKNrtJGxZXlhny
PIpciL0CgYEA01HmmXG1z82QmcvoMZBoHu9wNNl5krt5YsYUF9TWXTPijw6zxnLB
b0ef997KzMt1ZGRSq8Zae6YdMRXCvQJh1SMbhBTMfOwzbx+cqCcInmia47jAXgaV
agAc7im3gULeTnXtNRdo9cj2ilHmlJNFI0N0rl9NJxpYACEeSFaR6YMCgYEAxXD4
Z0+Boo+wsIvcxYWkYWpvhJfqqdETZJ5RdgkWLFG7OA2SWPgqIyWxPTnQRsK5ryjw
xyxNv7L9Ddd/+yCNsiuUIEdhLPtoIfBQ96722IJMTOxr9eWUXVngANBeMUypv/3q
v3AeD8MfTgJJPZ5F3zfLAeBPUxUx6JUIRxjFEq8CgYEAh+3q01EdjinAle1f0mH9
V8jU+GnbldMZ5btcOWgi65jwZu0iHsi6PIZqE9svwEQvowAVXYEduuPDa0uAFGxv
2dXXYUKvtruI6jX/YvqYxKys1UaGFvVNLv4bnecfrvoAXZ/lkX0ZeuBmFdtQ4slI
c8J0T6Xlzv1XFd43yHhHK1kCgYEAwwc0V9hRVSJahgmhqfq4xQE95tupENVVMq6w
CMgw/tY8+MFvLjL0bINu7+uLsFno0Py/2f4JTrKfBG06HfWqAbTKPJhFhQlRczTO
xdouOu96LwHaIqsEQrHkculgIZJ4mw1WNIOrLiavZX8cmaEdo8CY5uGLeaYWBogw
BQqSoEECgYEAuNXFrwDzUoqiFo9+7gjzT4pjQgh/zNTIODaXwIVBjTRe6Z4bQayd
Jel5Gf1IHu9iik6pvaVfK8tMc8eQisEC8F9U9l6mqw/Q4bpQ5k6CQtGe0roBEj2H
qJIQ/1TjMAOkOx7YiqTuFP4vs9LhCRAflQQ/Tg0fcHsMWBlPpWlEkRs=
-----END RSA PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
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
eFet28DEMzSifudXY3LYE5N0Vl6g+A==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEODCCAyACFCcgfqlRnT0lLgivo40jvyko/V4gMA0GCSqGSIb3DQEBCwUAMFcx
EjAQBgNVBAMMCWxvY2FsaG9zdDERMA8GA1UECgwITWFpbmZsdXgxDDAKBgNVBAsM
A0lvVDEgMB4GCSqGSIb3DQEJARYRaW5mb0BtYWluZmx1eC5jb20wHhcNMTkwNDAx
MDkyNzEyWhcNMjEwMzMxMDkyNzEyWjBaMRAwDgYDVQQDDAdkZWZhdWx0MREwDwYD
VQQKDAhNYWluZmx1eDERMA8GA1UECwwIbWFpbmZsdXgxIDAeBgkqhkiG9w0BCQEW
EWluZm9AbWFpbmZsdXguY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAuNyXOzUfhH4WOvXgaOIV+ViqXEXO/VfjxPBE4EOYxxk1S5N1tAM5STontEqJ
kv8sSInZkYPPPjcqv8yEh1q5GyZrsI3Th5wqovFQEKUwAP2m0bluJYwY0oqo8dcZ
vPhcMdEdJu1hzRI0LOiBv5EgkT6KT5z97CfDtV16uaVISywnQmo1TcY7tT0tQs2S
znc4kg6mbfGnbmkEHfiV5aOj4ahULd5grdPh+8YcnWXNE/maSZJLOKz2PIMuySS7
TmmmgJC6uMpsT5rCGI+WzNwYy8X6731zy/DVaMQ752QJfS0rUPwHa7KiEQNKAyrZ
NbYxiILKnbvd/JrN2iW6yllQDQn+XTft1aAui4fQcw/aBQZ5zbiedJeeDtKuve9+
X23PVhRumsI4Wfo4CzKBsdH6fT5oGOqmL8WFVCQl/p93vqPRbhadEeEqGgdE2om/
1SaA5nl7W4rbfo9beLpmi3KE+oldlLh5/mgH+7vWQQmmidC633AFaY7TabxU/59+
38Kzo6eAJauVoHFdXGCIgg/SemNS1KWo3t+pwBHJIPHdsLlWsRVtV5Vt8QW+MlD1
ODkApTstom0rtLvBoqBkI+2z29J+i07R4C2K/ZFdhv8Exf/MxUZeET+AznUwLHTE
SNxCsI/7wWQVyLVb1AwWLaBbt1cYd4YGVWe+QcslxNNayMkCAwEAATANBgkqhkiG
9w0BAQsFAAOCAQEAi7jvvUUMH2yVXfYgLUuBB8jRmwQcYKJo0jbPKZew07F+L3xM
WdYP+pDhdkyF79l99/fZS0Xs8dwYtAgU2tVkVoT6p/6vCvnqodgKgZJWi2dNCdG7
ftIJR9dkusHIy3cpSHNb+A/hYLvj1nY9IAmRiY1fBNrRflmQe73gUuIjuoqDQ8wV
5jteUUt33rH0wYhbMf4z9HFSDBK1Ti+Mw27ybDYnYb79FZjUnXAKR/Gb0QyyGQyI
N5sVboXyBEK6KlJ4xBQZ0gEvmhN0ZGgmje4u7+2E3pJxo3zRN8Qm5Poqyll+3Omd
3rPdUhkTrQhKC3iMi+hXr4ZjNSlcgF5f+zvRIA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
hqHUM7qo5k3nI6/dSdWQ1gBdmv104g==
-----END PRIVATE KEY-----

View File

@ -84,7 +84,7 @@ The most of the notifications received from the Adapter are non-confirmable. By
> Server must send a notification in a confirmable message instead of a non-confirmable message at least every 24 hours. This prevents a client that went away or is no longer interested from remaining in the list of observers indefinitely.
CoAP Adapter sends these notifications every 12 hours. To configure this period, please check [adapter documentation](../coap/README.md) If the client is no longer interested in receiving notifications, the second scenario described above can be used to unsubscribe
CoAP Adapter sends these notifications every 12 hours. To configure this period, please check (adapter documentation)[https://www.github.com/mainflux/mainflux/tree/master/coap/README.md) If the client is no longer interested in receiving notifications, the second scenario described above can be used to unsubscribe
## Subtopics

131
docs/security.md Normal file
View File

@ -0,0 +1,131 @@
# **SECURING COMMUNICATION**
By default gRPC communication is not secure as Mainflux system is most often run in a private network behind the reverse proxy.
However, TLS can be activated and configured.
# Server configuration
## Securing PostgreSQL connections
By default, Mainflux will connect to Postgres using insecure transport.
If a secured connection is required, you can select the SSL mode and set paths to any extra certificates and keys needed.
`MF_USERS_DB_SSL_MODE` the SSL connection mode for Users.
`MF_USERS_DB_SSL_CERT` the path to the certificate file for Users.
`MF_USERS_DB_SSL_KEY` the path to the key file for Users.
`MF_USERS_DB_SSL_ROOT_CERT` the path to the root certificate file for Users.
`MF_THINGS_DB_SSL_MODE` the SSL connection mode for Things.
`MF_THINGS_DB_SSL_CERT` the path to the certificate file for Things.
`MF_THINGS_DB_SSL_KEY` the path to the key file for Things.
`MF_THINGS_DB_SSL_ROOT_CERT` the path to the root certificate file for Things.
Supported database connection modes are: `disabled` (default), `required`, `verify-ca` and `verify-full`.
## Securing gRPC
### Users
If either the cert or key is not set, the server will use insecure transport.
`MF_USERS_SERVER_CERT` the path to server certificate in pem format.
`MF_USERS_SERVER_KEY` the path to the server key in pem format.
### Things
If either the cert or key is not set, the server will use insecure transport.
`MF_THINGS_SERVER_CERT` the path to server certificate in pem format.
`MF_THINGS_SERVER_KEY` the path to the server key in pem format.
## Client configuration
If you wish to secure the gRPC connection to `things` and `users` services you must define the CAs that you trust. This does not support mutual certificate authentication.
### HTTP Adapter
`MF_HTTP_ADAPTER_CA_CERTS` - the path to a file that contains the CAs in PEM format. If not set, the default connection will be insecure. If it fails to read the file, the adapter will fail to start up.
### Things
`MF_THINGS_CA_CERTS` - the path to a file that contains the CAs in PEM format. If not set, the default connection will be insecure. If it fails to read the file, the service will fail to start up.
# Mutual authentication
In the most of the cases, HTTPS, WSS, MQTTS or secure CoAP are secure enough. However, sometimes you might need even more secure connection. Mainflux supports mutual TLS authentication (_mTLS_) based on (X.509 certificates)[https://tools.ietf.org/html/rfc5280]. By default the TLS protocol only proves the identity of the server to the client using X.509 certificate and the authentication of the client to the server is left to the application layer. TLS also offers client-to-server authentication using client-side X.509 authentication. This is called two-way or mutual authentication. Mainflux currently supports mTLS over HTTP, WS, and MQTT protocols. In order to run Docker composition with mTLS turned on, you can execute following command from the project root:
```bash
AUTH=x509 docker-compose -f docker/docker-compose.yml up -d
```
Mutual authentication includes client side certificates. Certificates can be generated using simple script provided (here)[http://www.github.com/mainflux/mainflux/tree/master/docker/ssl/Makefile]. In order to create a valid certificate, you need to create Mainflux thing using the process described in the [provisioning section](provisioning.md). After that, you need to fetch created thing key. Thing key will be used to create x.509 certificate for corresponding thing. TO create certificate, execute following commands:
```bash
cd docker/ssl
make ca
make server_cert
make thing_cert KEY=<thing_key> CRT_FILE_NAME=<cert_name>
```
These commands use (OpenSSL)[https://www.openssl.org/] tool, so please make sure that you have it installed and set up before running these commands.
- Command `make ca` wil generate self-signed certificate that will later be used as a CA to sign other generated certificates. CA will expire in 3 years.
- Command `make server_cert` will generated and sign (with previously created CA) server cert, which will expire after 1000 days. This cert is used as a Mainflux server-side certificate in usual TLS flow to establish HTTPS, WSS, or MQTTS connection.
- Command `make thing_cert` wil finally generate and sign client-side certificate and private key for the thing.
In this example `<thing_key>` represents key of the thing, and `<cert_name>` represents name of the certificate and key file which will be saved in `docker/ssl/certs` directory. Generated Certificate will expire after 2 years. The key must be stored in the x.509 certificate "CN" field. This script is created for the testing purposes and is not meant to be used in production. We strongly recommend avoiding self-signed certificates and using certificate management tool such as (Vault)[https://www.vaultproject.io/] for the production.
Once you have created CA and server-side cert, you can spin the composition using:
```bash
AUTH=x509 docker-compose -f docker/docker-compose.yml up -d
```
Then, you can create user and provision things and channels. Now, in order to send a message from the specific thing to the channel, you need to connect thing to the channel and generate corresponding client certificate using aforementioned commands. To publish a message to the channel, thing should send following request:
_HTTPS:_
```bash
curl -s -S -i --cacert docker/ssl/certs/ca.crt --cert docker/ssl/certs/<thing_cert_name>.crt --key docker/ssl/certs/<thing_cert_key>.key --insecure -X POST -H "Content-Type: application/senml+json" https://localhost/http/channels/<channel_id>/messages -d '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
```
_MQTTS_:
###### PUBLISH
```bash
mosquitto_pub -u <thing_id> -P <thing_key> -t channels/<channel_id>/messages -h localhost --cafile docker/ssl/certs/ca.crt --cert docker/ssl/certs/<thing_cert_name>.crt --key docker/ssl/certs/<thing_cert_key>.key -m '[{"bn":"some-base-name:","bt":1.276020076001e+09, "bu":"A","bver":5, "n":"voltage","u":"V","v":120.1}, {"n":"current","t":-5,"v":1.2}, {"n":"current","t":-4,"v":1.3}]'
```
###### SUBSCRIBE
```
mosquitto_sub -u <thing_id> -P <thing_key> --cafile docker/ssl/certs/ca.crt --cert docker/ssl/certs/<thing_cert_name>.crt --key docker/ssl/certs/<thing_cert_key>.key -t channels/<channel_id>/messages -h localhost
```
_WSS:_
```javascript
const WebSocket = require('ws');
// Do not verify self-signed certificates if you are using one.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
// Replace <channel_id> and <thing_key> with real values.
const ws = new WebSocket('wss://localhost/ws/channels/<channel_id>/messages?authorization=<thing_key>',
// This is ClientOptions object that contains client cert and client key in the form of string. You can easily load these strings from cert and key files.
{
cert: `-----BEGIN CERTIFICATE-----....`,
key: `-----BEGIN RSA PRIVATE KEY-----.....`
})
ws.on('open', () => {
ws.send('something')
})
ws.on('message', (data) => {
console.log(data)
})
ws.on('error', (e) => {
console.log(e)
})
```
As you can see, `Authorization` header does not have to be present in the the HTTP request, since the key is present in the certificate. However, if yoy pass `Authorization` header, it _must be the same as the key in the cert_. In the case of MQTTS, `password` filed in CONNECT message _must match the key from the certificate_. In the case of WSS, `Authorization` header or `authorization` query parameter _must match cert key_.

View File

@ -1,50 +0,0 @@
By default gRPC communication is not secure as Mainflux system is most often run in a private network behind the reverse proxy.
However, TLS can be activated and configured.
## Server configuration
### Securing PostgreSQL connections
By default, Mainflux will connect to Postgres using insecure transport.
If a secured connection is required, you can select the SSL mode and set paths to any extra certificates and keys needed.
`MF_USERS_DB_SSL_MODE` the SSL connection mode for Users.
`MF_USERS_DB_SSL_CERT` the path to the certificate file for Users.
`MF_USERS_DB_SSL_KEY` the path to the key file for Users.
`MF_USERS_DB_SSL_ROOT_CERT` the path to the root certificate file for Users.
`MF_THINGS_DB_SSL_MODE` the SSL connection mode for Things.
`MF_THINGS_DB_SSL_CERT` the path to the certificate file for Things.
`MF_THINGS_DB_SSL_KEY` the path to the key file for Things.
`MF_THINGS_DB_SSL_ROOT_CERT` the path to the root certificate file for Things.
Supported database connection modes are: `disabled` (default), `required`, `verify-ca` and `verify-full`
### Users
If either the cert or key is not set, the server will use insecure transport.
`MF_USERS_SERVER_CERT` the path to server certificate in pem format.
`MF_USERS_SERVER_KEY` the path to the server key in pem format.
### Things
If either the cert or key is not set, the server will use insecure transport.
`MF_THINGS_SERVER_CERT` the path to server certificate in pem format.
`MF_THINGS_SERVER_KEY` the path to the server key in pem format.
## Client configuration
If you wish to secure the gRPC connection to `things` and `users` services you must define the CAs that you trust. This does not support mutual certificate authentication.
### HTTP Adapter
`MF_HTTP_ADAPTER_CA_CERTS` - the path to a file that contains the CAs in PEM format. If not set, the default connection will be insecure. If it fails to read the file, the adapter will fail to start up.
### Things
`MF_THINGS_CA_CERTS` - the path to a file that contains the CAs in PEM format. If not set, the default connection will be insecure. If it fails to read the file, the service will fail to start up.

View File

@ -33,7 +33,7 @@ pages:
- Messaging: messaging.md
- Storage: storage.md
- LoRa: lora.md
- TLS: tls.md
- Security: security.md
- CLI: cli.md
- Bootstrap: bootstrap.md
- Developer's Guide: dev-guide.md