diff --git a/README.md b/README.md index 84f8972..af529b9 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ For more information on how to generate these files, please consult the official [Docker documentation](https://docs.docker.com/engine/security/protect-access/). The files can be uploaded to the device using HTTP. -The dockerd service will restart, or try to start, after each HTTP POST request. +The request will be rejected if the file being uploaded has the incorrect header or footer for that file type. +The dockerd service will restart, or try to start, after each successful HTTP POST request. ```sh curl --anyauth -u "root:$DEVICE_PASSWORD" -F file=@ca.pem -X POST \ @@ -209,6 +210,21 @@ has a significantly higher inference time when using a small and slow SD card. To get more informed about specifications, check the [SD Card Standards](https://www.sdcard.org/developers/sd-standard-overview/). +## Additional configuration + +For even more control over the dockerd daemon, +a configuration file can be uploaded to the device using HTTP. + +```sh +curl --anyauth -u "root:$DEVICE_PASSWORD" -F file=@daemon.json -X POST \ + http://$DEVICE_IP/local/dockerdwrapper/daemon.json +``` + +The complete specification of this file can be found in the Docker reference, in section +[Daemon configuration file](https://docs.docker.com/reference/cli/dockerd/#daemon-configuration-file). + +The dockerd service will automatically restart after a new configuration file has been uploaded. + ## Using the Docker ACAP The Docker ACAP does not contain the docker client binary. This means that all diff --git a/app/app_paths.h b/app/app_paths.h index a25d300..361992b 100644 --- a/app/app_paths.h +++ b/app/app_paths.h @@ -2,3 +2,5 @@ #define APP_DIRECTORY "/usr/local/packages/" APP_NAME #define APP_LOCALDATA APP_DIRECTORY "/localdata" + +#define DAEMON_JSON "daemon.json" diff --git a/app/dockerdwrapper.c b/app/dockerdwrapper.c index 1fc11fd..d163a66 100644 --- a/app/dockerdwrapper.c +++ b/app/dockerdwrapper.c @@ -487,7 +487,7 @@ static bool start_dockerd(const struct settings* settings, struct app_state* app args_len - args_offset, "%s %s", "dockerd", - "--config-file " APP_LOCALDATA "/daemon.json"); + "--config-file " APP_LOCALDATA "/" DAEMON_JSON); g_strlcpy(msg, "Starting dockerd", msg_len); diff --git a/app/html/index.html b/app/html/index.html index 60412d6..54261c6 100644 --- a/app/html/index.html +++ b/app/html/index.html @@ -15,5 +15,10 @@

Remove TLS certificates and keys

curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -X DELETE http://$DEVICE_IP/local/dockerdwrapper/server-cert.pem
curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -X DELETE http://$DEVICE_IP/local/dockerdwrapper/server-key.pem
+

Check and upload dockerd configuration

+ + curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD http://$DEVICE_IP/local/dockerdwrapper/daemon.json
+ curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -F file=@daemon.json -X POST http://$DEVICE_IP/local/dockerdwrapper/daemon.json.pem
+
diff --git a/app/http_request.c b/app/http_request.c index 1f6cd8c..b53acef 100644 --- a/app/http_request.c +++ b/app/http_request.c @@ -2,12 +2,14 @@ #include "app_paths.h" #include "fcgi_write_file_from_stream.h" #include "log.h" +#include "tls.h" #include #include #define HTTP_200_OK "200 OK" #define HTTP_204_NO_CONTENT "204 No Content" #define HTTP_400_BAD_REQUEST "400 Bad Request" +#define HTTP_403_FORBIDDEN "403 Forbidden" #define HTTP_404_NOT_FOUND "404 Not Found" #define HTTP_405_METHOD_NOT_ALLOWED "405 Method Not Allowed" #define HTTP_422_UNPROCESSABLE_CONTENT "422 Unprocessable Content" @@ -51,6 +53,12 @@ static bool remove_from_localdata(const char* filename) { return success; } +// Certificate files have more restrictions on them than daemon.json. This function is only designed +// for filenames exposed in the httpConfig part of the manifest. +static bool cert_filename(const char* filename) { + return strcmp(filename, DAEMON_JSON) != 0; +} + static void response(FCGX_Request* request, const char* status, const char* content_type, const char* body) { FCGX_FPrintF(request->out, @@ -81,7 +89,11 @@ post_request(FCGX_Request* request, const char* filename, restart_dockerd_t rest response_msg(request, HTTP_422_UNPROCESSABLE_CONTENT, "Upload to temporary file failed."); return; } - if (!copy_to_localdata(temp_file, filename)) + if (cert_filename(filename) && !tls_file_has_correct_format(filename, temp_file)) { + g_autofree char* msg = + g_strdup_printf("File is not a valid %s.", tls_file_description(filename)); + response_msg(request, HTTP_400_BAD_REQUEST, msg); + } else if (!copy_to_localdata(temp_file, filename)) response_msg(request, HTTP_500_INTERNAL_SERVER_ERROR, "Failed to copy file to localdata"); else { response_204_no_content(request); @@ -92,6 +104,25 @@ post_request(FCGX_Request* request, const char* filename, restart_dockerd_t rest log_error("Failed to remove %s: %s", temp_file, strerror(errno)); } +static void get_request(FCGX_Request* request, const char* filename) { + if (strcmp(filename, DAEMON_JSON) != 0) { + response_msg(request, HTTP_403_FORBIDDEN, "Resource is write-only"); + return; + } + + g_autofree char* contents = NULL; + gsize length; + GError* error = NULL; + const char* full_path = APP_LOCALDATA "/" DAEMON_JSON; + if (!g_file_get_contents(full_path, &contents, &length, &error)) { + log_error("Failed to read %s: %s.", full_path, error->message); + response_msg(request, HTTP_404_NOT_FOUND, "Could not read file"); + return; + } + + response(request, HTTP_200_OK, "application/json", contents); +} + static void delete_request(FCGX_Request* request, const char* filename) { if (!exists_in_localdata(filename)) response_msg(request, HTTP_404_NOT_FOUND, "File not found in localdata"); @@ -129,6 +160,8 @@ void http_request_callback(void* request_void_ptr, void* restart_dockerd_void_pt if (strcmp(method, "POST") == 0) post_request(request, filename, restart_dockerd_void_ptr); + else if (strcmp(method, "GET") == 0) + get_request(request, filename); else if (strcmp(method, "DELETE") == 0) delete_request(request, filename); else diff --git a/app/manifest.json b/app/manifest.json index 726a75a..4d1eb3b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -58,6 +58,11 @@ ], "settingPage": "index.html", "httpConfig": [ + { + "access": "admin", + "name": "daemon.json", + "type": "fastCgi" + }, { "access": "admin", "name": "ca.pem", diff --git a/app/tls.c b/app/tls.c index 2b6f1e2..6039bc1 100644 --- a/app/tls.c +++ b/app/tls.c @@ -2,6 +2,7 @@ #include "app_paths.h" #include "log.h" #include +#include #include #define TLS_CERT_PATH APP_LOCALDATA @@ -18,6 +19,17 @@ static struct cert tls_certs[] = {{"--tlscacert", "ca.pem", "CA certificate"}, #define NUM_TLS_CERTS (sizeof(tls_certs) / sizeof(tls_certs[0])) +#define BEGIN(x) "-----BEGIN " x "-----\n" +#define END(x) "-----END " x "-----\n" +#define CERTIFICATE "CERTIFICATE" +#define PRIVATE_KEY "PRIVATE KEY" +#define RSA_PRIVATE_KEY "RSA PRIVATE KEY" + +// Filename is assumed to be one of those listed in tls_certs[]. +static bool is_key_file(const char* filename) { + return strstr(filename, "key"); +} + static bool cert_file_exists(const struct cert* tls_cert) { g_autofree char* full_path = g_strdup_printf("%s/%s", TLS_CERT_PATH, tls_cert->filename); return access(full_path, F_OK) == 0; @@ -39,6 +51,13 @@ void tls_log_missing_cert_warnings(void) { tls_certs[i].filename); } +const char* tls_file_description(const char* filename) { + for (size_t i = 0; i < NUM_TLS_CERTS; ++i) + if (strcmp(filename, tls_certs[i].filename) == 0) + return tls_certs[i].description; + return NULL; +} + const char* tls_args_for_dockerd(void) { static char args[512]; // Too small buffer will cause truncated options, nothing more. const char* end = args + sizeof(args); @@ -53,3 +72,52 @@ const char* tls_args_for_dockerd(void) { tls_certs[i].filename); return args; } + +static bool read_bytes_from(FILE* fp, int whence, char* buffer, int num_bytes) { + const long offset = whence == SEEK_SET ? 0 : -num_bytes; + if (fseek(fp, offset, whence) != 0) { + log_error("Could not reposition stream to %s%ld: %s", + whence == SEEK_SET ? "SEEK_SET+" : "SEEK_END", + offset, + strerror(errno)); + return false; + } + if (fread(buffer, num_bytes, 1, fp) != 1) { + log_error("Could not read %d bytes: %s", num_bytes, strerror(errno)); + return false; + } + return true; +} + +static bool is_file_section_equal_to(FILE* fp, int whence, const char* section) { + char buffer[128]; + int to_read = strlen(section); + if (!read_bytes_from(fp, whence, buffer, to_read)) + return false; + buffer[to_read] = '\0'; + return strncmp(buffer, section, to_read) == 0; +} + +static bool has_header_and_footer(FILE* fp, const char* header, const char* footer) { + return is_file_section_equal_to(fp, SEEK_SET, header) && + is_file_section_equal_to(fp, SEEK_END, footer); +} + +bool tls_file_has_correct_format(const char* filename, const char* path_to_file) { + FILE* fp = fopen(path_to_file, "r"); + if (!fp) { + log_error("Could not read %s", path_to_file); + return false; + } + + bool correct = is_key_file(filename) + ? (has_header_and_footer(fp, BEGIN(PRIVATE_KEY), END(PRIVATE_KEY)) || + has_header_and_footer(fp, BEGIN(RSA_PRIVATE_KEY), END(RSA_PRIVATE_KEY))) + : has_header_and_footer(fp, BEGIN(CERTIFICATE), END(CERTIFICATE)); + if (!correct) + log_error("%s does not contain the headers and footers for a %s.", + path_to_file, + tls_file_description(filename)); + fclose(fp); + return correct; +} diff --git a/app/tls.h b/app/tls.h index 60e3203..2d951d3 100644 --- a/app/tls.h +++ b/app/tls.h @@ -3,4 +3,6 @@ bool tls_missing_certs(void); void tls_log_missing_cert_warnings(void); +const char* tls_file_description(const char* filename); const char* tls_args_for_dockerd(void); +bool tls_file_has_correct_format(const char* filename, const char* path_to_file);