diff --git a/epicardium/main.c b/epicardium/main.c
index 56639ea56ead01fc06138e0f2f712518193c475f..b3b94732576d696a1cfbf11f6207ca81c6cc73c0 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -1,6 +1,7 @@
 #include "modules/modules.h"
 #include "modules/log.h"
 #include "modules/filesystem.h"
+#include "modules/config.h"
 #include "card10-version.h"
 
 #include "FreeRTOS.h"
@@ -20,6 +21,8 @@ int main(void)
 	LOG_DEBUG("startup", "Initializing hardware ...");
 	hardware_early_init();
 
+	load_config();
+
 	/*
 	 * Version Splash
 	 */
diff --git a/epicardium/meson.build b/epicardium/meson.build
index cf67023b9f7143336875d973108d8ee0a080ffd1..a6a6c05261c16ced283c4d8591b335237f6acd37 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -70,7 +70,7 @@ subdir('ble/')
 
 subdir('l0der/')
 
-epicardium_cargs = []
+epicardium_cargs = ['-D_POSIX_C_SOURCE=200809']
 if get_option('jailbreak_card10')
   epicardium_cargs += [
     '-DJAILBREAK_CARD10=1',
diff --git a/epicardium/modules/config.c b/epicardium/modules/config.c
new file mode 100644
index 0000000000000000000000000000000000000000..47396e2195b8b262f41279dbdad4e588430efdef
--- /dev/null
+++ b/epicardium/modules/config.c
@@ -0,0 +1,347 @@
+#include "modules/log.h"
+#include "modules/config.h"
+#include "modules/filesystem.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#define CONFIG_MAX_LINE_LENGTH 80
+
+enum OptionType {
+	OptionType_Boolean,
+	OptionType_Int,
+	OptionType_Float,
+	OptionType_String,
+};
+
+struct config_option {
+	const char *name;
+	enum OptionType type;
+	union {
+		bool boolean;
+		long integer;
+		double floating_point;
+		char *string;
+	} value;
+};
+
+static struct config_option s_options[_EpicOptionCount] = {
+/* clang-format off */
+	#define INIT_Boolean(v)		        { .boolean        = (v) }
+	#define INIT_Int(v)  		        { .integer        = (v) }
+	#define INIT_Float(v)  		        { .floating_point = (v) }
+	#define INIT_String(v)  	        { .string         = (v) }
+	#define INIT_(tp, v)                INIT_ ## tp (v)
+	#define INIT(tp, v)                 INIT_ (tp, v)
+
+	#define CARD10_SETTING(identifier, spelling, tp, default_value)     \
+		[Option ## identifier] = { .name  = (spelling),                 \
+					               .type  = OptionType_ ## tp,          \
+					               .value = INIT(tp, (default_value)) },
+
+	#include "modules/config.def"
+	/* clang-format on */
+};
+
+static struct config_option *findOption(const char *key)
+{
+	for (int i = 0; i < _EpicOptionCount; ++i) {
+		if (!strcmp(key, s_options[i].name)) {
+			return &s_options[i];
+		}
+	}
+	return NULL;
+}
+
+static bool set_bool(struct config_option *opt, const char *value)
+{
+	bool val;
+	if (!strcmp(value, "1")) {
+		val = true;
+	} else if (!strcmp(value, "true")) {
+		val = true;
+	} else if (!strcmp(value, "0")) {
+		val = false;
+	} else if (!strcmp(value, "false")) {
+		val = false;
+	} else {
+		return false;
+	}
+	opt->value.boolean = val;
+	LOG_DEBUG(
+		"card10.cfg",
+		"setting '%s' to %s",
+		opt->name,
+		val ? "true" : "false"
+	);
+	return true;
+}
+
+static bool set_int(struct config_option *opt, const char *value)
+{
+	char *endptr;
+	size_t len = strlen(value);
+	int v      = strtol(value, &endptr, 0);
+	if (endptr != (value + len)) {
+		return false;
+	}
+	opt->value.integer = v;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %d (0x%08x)", opt->name, v, v);
+	return true;
+}
+
+static bool set_float(struct config_option *opt, const char *value)
+{
+	char *endptr;
+	size_t len = strlen(value);
+	double v   = strtod(value, &endptr);
+	if (endptr != (value + len)) {
+		return false;
+	}
+	opt->value.floating_point = v;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %f", opt->name, v);
+	return true;
+}
+
+const char *elide(const char *str)
+{
+	static char ret[21];
+	size_t len = strlen(str);
+	if (len <= 20) {
+		return str;
+	}
+	strncpy(ret, str, 17);
+	ret[17] = '.';
+	ret[18] = '.';
+	ret[19] = '.';
+	ret[20] = '\0';
+	return ret;
+}
+
+static bool set_string(struct config_option *opt, const char *value)
+{
+	//this leaks, but the lifetime of these ends when epicardium exits, so...
+	char *leaks       = strdup(value);
+	opt->value.string = leaks;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks));
+	return true;
+}
+
+static void configure(const char *key, const char *value, int lineNumber)
+{
+	struct config_option *opt = findOption(key);
+	if (!opt) {
+		//invalid key
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: ignoring unknown option '%s'",
+			lineNumber,
+			key
+		);
+		return;
+	}
+	bool ok = false;
+	switch (opt->type) {
+	case OptionType_Boolean:
+		ok = set_bool(opt, value);
+		break;
+	case OptionType_Int:
+		ok = set_int(opt, value);
+		break;
+	case OptionType_Float:
+		ok = set_float(opt, value);
+		break;
+	case OptionType_String:
+		ok = set_string(opt, value);
+		break;
+	default:
+		assert(0 && "unreachable");
+	}
+	if (!ok) {
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: ignoring invalid value '%s' for option '%s'",
+			lineNumber,
+			value,
+			key
+		);
+	}
+}
+
+static void doline(char *line, char *eol, int lineNumber)
+{
+	//skip leading whitespace
+	while (*line && isspace(*line))
+		++line;
+
+	char *key = line;
+	if (*key == '#') {
+		//skip comments
+		return;
+	}
+
+	char *eq = strchr(line, '=');
+	if (!eq) {
+		if (*key) {
+			LOG_WARN(
+				"card10.cfg",
+				"line %d (%s): syntax error",
+				lineNumber,
+				elide(line)
+			);
+		}
+		return;
+	}
+
+	char *e_key = eq - 1;
+	//skip trailing whitespace in key
+	while (e_key > key && isspace(*e_key))
+		--e_key;
+	e_key[1] = '\0';
+	if (*key == '\0') {
+		LOG_WARN("card10.cfg", "line %d: empty key", lineNumber);
+		return;
+	}
+
+	char *value = eq + 1;
+	//skip leading whitespace
+	while (*value && isspace(*value))
+		++value;
+
+	char *e_val = eol - 1;
+	//skip trailing whitespace
+	while (e_val > value && isspace(*e_val))
+		--e_val;
+	if (*value == '\0') {
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: empty value for option '%s'",
+			lineNumber,
+			key
+		);
+		return;
+	}
+
+	configure(key, value, lineNumber);
+}
+
+bool config_get_boolean(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Boolean);
+	return opt->value.boolean;
+}
+
+long config_get_integer(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Int);
+	return opt->value.integer;
+}
+
+double config_get_float(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_Float);
+	return opt->value.floating_point;
+}
+
+const char *config_get_string(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_String);
+	return opt->value.string;
+}
+
+void load_config(void)
+{
+	LOG_DEBUG("card10.cfg", "loading...");
+	int fd = epic_file_open("card10.cfg", "r");
+	if (fd < 0) {
+		LOG_DEBUG(
+			"card10.cfg",
+			"loading failed: %s (%d)",
+			strerror(-fd),
+			fd
+		);
+		return;
+	}
+	char buf[CONFIG_MAX_LINE_LENGTH];
+	int lineNumber = 0;
+	int nread;
+	do {
+		//zero-terminate in case file is empty
+		buf[0] = '\0';
+		nread  = epic_file_read(fd, buf, sizeof(buf));
+		if (nread < sizeof(buf)) {
+			//add fake EOL to ensure termination
+			buf[nread] = '\n';
+		}
+		char *line   = buf;
+		char *eol    = NULL;
+		int last_eol = 0;
+		while (line) {
+			//line points one character past the las (if any) '\n' hence '- 1'
+			last_eol = line - buf - 1;
+			eol      = strchr(line, '\n');
+			++lineNumber;
+			if (eol) {
+				*eol = '\0';
+				doline(line, eol, lineNumber);
+				line = eol + 1;
+			} else {
+				if (line == buf) {
+					//line did not fit into buf
+					LOG_WARN(
+						"card10.cfg",
+						"line:%d: too long - aborting",
+						lineNumber
+					);
+					return;
+				} else {
+					int seek_back = last_eol - nread;
+					LOG_DEBUG(
+						"card10.cfg",
+						"nread, last_eol, seek_back: %d,%d,%d",
+						nread,
+						last_eol,
+						seek_back
+					);
+					assert(seek_back <= 0);
+					if (seek_back) {
+						int rc = epic_file_seek(
+							fd,
+							seek_back,
+							SEEK_CUR
+						);
+						if (rc < 0) {
+							LOG_ERR("card10.cfg",
+								"seek failed, aborting");
+							return;
+						}
+						char newline;
+						rc = epic_file_read(
+							fd, &newline, 1
+						);
+						if (rc < 0 || newline != '\n') {
+							LOG_ERR("card10.cfg",
+								"seek failed, aborting");
+							LOG_DEBUG(
+								"card10.cfg",
+								"seek failed at read-back of newline: rc: %d read: %d",
+								rc,
+								(int)newline
+							);
+							return;
+						}
+					}
+					break;
+				}
+			}
+		}
+	} while (nread == sizeof(buf));
+}
diff --git a/epicardium/modules/config.def b/epicardium/modules/config.def
new file mode 100644
index 0000000000000000000000000000000000000000..455aaedd8d61f821c1d08ee99bb45c3a61d983ba
--- /dev/null
+++ b/epicardium/modules/config.def
@@ -0,0 +1,11 @@
+#ifndef CARD10_SETTING
+#  define CARD10_SETTING(identifier, spelling, type, default_value)
+#endif
+
+CARD10_SETTING(ExecuteElf, "execute_elf", Boolean, false)
+//CARD10_SETTING(Nick, "nick", String, "an0n")
+//CARD10_SETTING(Timeout, "timeout", Integer, 123)
+//CARD10_SETTING(Dampening, "dampening", Float, 420)
+
+
+#undef CARD10_SETTING
diff --git a/epicardium/modules/config.h b/epicardium/modules/config.h
new file mode 100644
index 0000000000000000000000000000000000000000..b72b08a5205bf9344bc3e3d805423dc826f30ccc
--- /dev/null
+++ b/epicardium/modules/config.h
@@ -0,0 +1,21 @@
+#ifndef EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+#define EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+
+#include <stdbool.h>
+
+enum EpicConfigOption {
+    #define CARD10_SETTING(identifier, spelling, type, default_value) Option ## identifier,
+	#include "modules/config.def"
+    _EpicOptionCount
+};
+
+//initialize configuration values and load card10.cfg
+void load_config(void);
+
+bool config_get_boolean(enum EpicConfigOption option);
+long config_get_integer(enum EpicConfigOption option);
+double config_get_float(enum EpicConfigOption option);
+const char* config_get_string(enum EpicConfigOption option);
+
+
+#endif//EPICARDIUM_MODULES_CONFIG_H_INCLUDED
diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c
index 375cfef61dd9972fbb418f72659a1e30e69ef615..650664d2e97ee15ecbd35199e176ded925a78bfb 100644
--- a/epicardium/modules/lifecycle.c
+++ b/epicardium/modules/lifecycle.c
@@ -1,6 +1,7 @@
 #include "epicardium.h"
 #include "modules/log.h"
 #include "modules/modules.h"
+#include "modules/config.h"
 #include "api/dispatcher.h"
 #include "api/interrupt-sender.h"
 #include "l0der/l0der.h"
@@ -49,6 +50,7 @@ static volatile struct load_info async_load = {
 
 /* Whether to write the menu script before attempting to load. */
 static volatile bool write_menu = false;
+static bool execute_elfs        = false;
 
 /* Helpers {{{ */
 
@@ -88,9 +90,7 @@ static int load_stat(char *name)
  */
 static int do_load(struct load_info *info)
 {
-#if defined(JAILBREAK_CARD10) && (JAILBREAK_CARD10 == 1)
 	struct l0dable_info l0dable;
-#endif
 	int res;
 
 	if (*info->name == '\0') {
@@ -129,18 +129,22 @@ static int do_load(struct load_info *info)
 	case PL_PYTHON_INTERP:
 		core1_load(PYCARDIUM_IVT, info->name);
 		break;
-#if defined(JAILBREAK_CARD10) && (JAILBREAK_CARD10 == 1)
 	case PL_L0DABLE:
-		res = l0der_load_path(info->name, &l0dable);
-		if (res != 0) {
-			LOG_ERR("lifecycle", "l0der failed: %d\n", res);
-			xSemaphoreGive(api_mutex);
-			return -ENOEXEC;
+		if (execute_elfs) {
+			res = l0der_load_path(info->name, &l0dable);
+			if (res != 0) {
+				LOG_ERR("lifecycle", "l0der failed: %d\n", res);
+				xSemaphoreGive(api_mutex);
+				return -ENOEXEC;
+			}
+			core1_load(l0dable.isr_vector, "");
+		} else {
+			LOG_WARN(
+				"lifecycle",
+				"Execution of .elf l0dables is disabled"
+			);
 		}
-		core1_load(l0dable.isr_vector, "");
-
 		break;
-#endif
 	default:
 		LOG_ERR("lifecyle",
 			"Attempted to load invalid payload (%s)",
@@ -379,6 +383,8 @@ void vLifecycleTask(void *pvParameters)
 
 	hardware_init();
 
+	execute_elfs = config_get_boolean(OptionExecuteElf);
+
 	/* When triggered, reset core 1 to menu */
 	while (1) {
 		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 693ca9a8f94a432ff64d770f91d9e9005c88d42a..628249a305098911b6dfe3c861b8759d6d5a5a62 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -21,5 +21,6 @@ module_sources = files(
   'trng.c',
   'vibra.c',
   'watchdog.c',
-  'usb.c'
+  'usb.c',
+  'config.c',
 )