New feature; add encrypt-at-rest

When a jail is encryted at rest using 'encfs' we detect that and ask for
a password upon starting the jail.

This sounded like a neat little idea which ended up taking nearly 4 days
to do...
EncFS needs to be running as root, as it is a FUSE system and it will
actually stop root from reading/writing files if it is running as a
user. It also is very picky about not running in a namespace, it manages
to hang indefinitely otherwise where a shutdown can't complete because
the process doesn't want to die :-)

So, it runs as root, takes the password via a pipe and we have a
watchdog proces to kill it when the jail is shut down.
This commit is contained in:
2024-05-17 11:39:44 +02:00
parent 4c1505b8fa
commit d046c171f6
10 changed files with 266 additions and 17 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_STANDARD 17)
find_package(Qt6 COMPONENTS Core DBus REQUIRED)
find_package(Qt6 COMPONENTS Core Widgets DBus REQUIRED)
# starting with Qt5.15 we have a lot of deprecation warnings,
# likely to make porting to Qt6 easier.
@@ -24,7 +24,7 @@ set (SERVER_SOURCES
)
add_executable(isolation_runner ${SERVER_SOURCES})
target_link_libraries(isolation_runner Qt6::Core Qt6::DBus)
target_link_libraries(isolation_runner Qt6::Core Qt6::DBus Qt6::Widgets)
if ("$ENV{HOME}" STREQUAL "/root") # hacky way to know if we're root.
# setuid is needed, we can apply that when root installs it.
+46
View File
@@ -12,6 +12,7 @@
#include <QStringBuilder>
#include <QStandardPaths>
#include <QTimer>
#include <QInputDialog>
class RulesError : public std::runtime_error
{
@@ -67,10 +68,20 @@ QString IsolationManager::startApplicationRequest(AppEntry &dbEntry, const QStri
if (!base.mkpath(homedir))
return QString("Internal error: failed to create environment");
if (dbEntry.jailPassword.isEmpty()
&& QFile::exists(m_basedir % "/." % homedir % "/.encfs6.xml")) {
auto *da = new DelayedApp(dbEntry, arguments, this);
da->askPassword();
return QString();
}
// then ask the priviledged task to take it from here.
Message message(Message::MAX_SIZE);
message.setJailId(dbEntry.appId);
try {
if (!dbEntry.jailPassword.isEmpty())
message.setJailPassword(dbEntry.jailPassword.toStdString());
QString exe = dbEntry.pathToExe;
// resolve symlinks and actually find the real executable.
for (int i = 0; i < 10; ++i) { // avoid endless loops
@@ -515,3 +526,38 @@ void AutoDeleter::jailClosed(const QString &pipeFile)
deleteLater();
}
DelayedApp::DelayedApp(IsolationManager::AppEntry appEntry, const QStringList &arguments, IsolationManager *parent)
: m_jail(appEntry),
m_parent(parent),
m_arguments(arguments)
{
assert(parent);
}
void DelayedApp::askPassword()
{
auto id = new QInputDialog();
m_win = id;
id->setInputMode(QInputDialog::TextInput);
id->setTextEchoMode(QLineEdit::Password);
id->setLabelText("ISO password:");
id->setWindowTitle("Jail Requires Password");
id->open(this, SLOT(passwordEntered(QString)));
connect (id, SIGNAL(rejected()), this, SLOT(cancelPressed()));
}
void DelayedApp::cancelPressed()
{
qWarning() << "canceled";
m_win->deleteLater();
deleteLater();
}
void DelayedApp::passwordEntered(const QString &text)
{
m_win->deleteLater();
m_jail.jailPassword = text;
m_parent->startApplicationRequest(m_jail, m_arguments);
deleteLater();
}
+24
View File
@@ -9,6 +9,9 @@
#include <QDir>
#include <QFileSystemWatcher>
class QWidget;
/**
* The isolation-manager is the biggest part of the
* server. It is the listener and it drops root priviledges
@@ -32,6 +35,7 @@ public:
QStringList denied;
QStringList allowed;
QString initScript;
QString jailPassword;
bool autoDelete = false;
// defaults as read from the rules file
@@ -107,4 +111,24 @@ private:
QFileSystemWatcher m_watcher;
};
class DelayedApp : public QObject
{
Q_OBJECT
public:
explicit DelayedApp(IsolationManager::AppEntry appEntry, const QStringList &arguments, IsolationManager *parent);
void askPassword();
private slots:
void cancelPressed();
void passwordEntered(const QString &text);
private:
IsolationManager *m_parent;
IsolationManager::AppEntry m_jail;
const QStringList m_arguments;
QWidget *m_win = nullptr;
};
#endif
+13
View File
@@ -11,6 +11,7 @@ enum FieldType {
ExecutablePath = 10,
Argument,
InitScript,
JailPassword, // The jaildir is encfs encrypted. Decrypt password.
IsTry = 20, // allow next command to fail
RBindMountSource, // mount from an existing directory.
@@ -247,6 +248,11 @@ void Message::addEnvToSet(const std::string &envVar)
addString(EnvironSet, envVar);
}
void Message::setJailPassword(const std::string &pwd)
{
addString(JailPassword, pwd);
}
void Message::addDBusProxy(DBusType type, const std::string &from, const std::string &to)
{
addString(type == UserSessionBus ? DBusProxyFrom : DBusProxySystemFrom, from);
@@ -342,6 +348,12 @@ bool Message::Iterator::isInitSript() const
return m_cur[0] == InitScript;
}
bool Message::Iterator::isJailPwd() const
{
assert(isValid());
return m_cur[0] == JailPassword;
}
bool Message::Iterator::isValid() const
{
assert(m_parent);
@@ -494,6 +506,7 @@ bool Message::Iterator::next()
case EnvironSet: // fall through
case EnvironUnset: // fall through
case InitScript: // fall through
case JailPassword: // fall through
case CreateTmpFs:
m_recordSize = ::stringLength(m_cur + 1, end) + 2;
break;
+2
View File
@@ -79,6 +79,7 @@ public:
void addEnvToUnset(const std::string &propertyName);
void addEnvToSet(const std::string &envVar);
void setJailPassword(const std::string &pwd);
enum DBusType {
UserSessionBus,
@@ -99,6 +100,7 @@ public:
bool isCopy() const;
bool isJailId() const;
bool isInitSript() const;
bool isJailPwd() const;
bool isValid() const;
bool isTry() const {
return m_isTry;
+1 -1
View File
@@ -22,7 +22,7 @@ RemoteRunner::~RemoteRunner()
m_thread.wait();
}
void RemoteRunner::runRemote(const Message &message)
void RemoteRunner::runRemote(const Message &message) const
{
assert(message.size() > 0);
assert(message.size() < 0x7FFF);
+1 -1
View File
@@ -47,7 +47,7 @@ public:
RemoteRunner(int inputId, int outputId);
~RemoteRunner();
void runRemote(const Message &message);
void runRemote(const Message &message) const;
signals:
void receivedMessage(QByteArray data);
+170 -11
View File
@@ -13,10 +13,14 @@
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>
#include <atomic>
#define PIPE_READ 0
#define PIPE_WRITE 1
// the waiting thread.
void waitForChildren(pid_t pid)
void waitForChildren(pid_t pid, const int encFsWatchDogPipe)
{
static std::atomic_int childCount(0);
childCount.fetch_add(1);
@@ -37,14 +41,32 @@ void waitForChildren(pid_t pid)
// realistically, the next line is irrelevant since nobody is listening.
fprintf(stderr, "failed to remove iso-pipe. Error: %s\n", e.what());
}
if (encFsWatchDogPipe)
write(encFsWatchDogPipe, "go!", 3); // the watchdog sends the kill signal to the encFS process.
exit(0);
}
}
void waitForEncFs(pid_t pid)
{
int status;
waitpid(pid, &status, 0);
// if the watched encfs process dies, typically because of an
// incorrect password, the starting of the jail should be cancelled.
exit(0);
}
Runner::Runner(const Message &message, int errorFile)
: m_outputFD(errorFile),
m_message(message)
{
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
if (i.isJailId()) {
m_jailId = i.jailId();
break;
}
}
}
void Runner::setOwnerUserId(uint32_t uid)
@@ -80,6 +102,23 @@ void Runner::run()
close(fd); // cleanup
}
/*
* While still fully root, start the encfs base, if needed.
*/
int encFSWatchDog = 0;
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
if (i.isJailPwd()) {
// we decrypt the jail with the password given.
encFSWatchDog = runEncFs(i.stringPtr(), i.stringLength());
if (encFSWatchDog == 0) {
fprintf(stderr, "Failed to run encfs\n");
exit(18);
}
break;
}
}
/*
* 'unshare' is the nicest way to run a program in a new namespace.
* PID 'unshare' means that the to-be-run application can not see which other
@@ -321,7 +360,7 @@ void Runner::run()
if (pid) { // parent
// printf("Fork 3 done, created %d (I'm %d). waiting for child to exit\n", pid, getpid());
renameThisProcess(m_processName, m_processNameSize, "jailer");
new std::thread(waitForChildren, pid);
new std::thread(waitForChildren, pid, encFSWatchDog);
// create the pipe that indicates this jail is occupied.
// the pipe can be used by the dispatcher to send us more things to run
@@ -360,7 +399,7 @@ void Runner::run()
if (pid == -1) exit(1);
if (pid) { // I'm parent
// pid is child's pid, lets wait for them in a thread.
new std::thread(waitForChildren, pid);
new std::thread(waitForChildren, pid, encFSWatchDog);
}
else { // run up to exec, below!
m_message = Message(buf, msgSize);
@@ -405,15 +444,8 @@ void Runner::run()
void Runner::sendUpstream(const char *errorMessage)
{
assert(errorMessage);
uint32_t jailId = 0;
for (auto i = m_message.iBegin(); i.isValid(); i.next()) {
if (i.isJailId()) {
jailId = i.jailId();
break;
}
}
char messageBuf[20];
int size = snprintf(messageBuf, sizeof(messageBuf) - 1, "%u ", jailId);
int size = snprintf(messageBuf, sizeof(messageBuf) - 1, "%u ", m_jailId);
write(m_outputFD, messageBuf, size);
const int len = strlen(errorMessage);
write(m_outputFD, errorMessage, len + 1); // including trailing zero
@@ -588,6 +620,133 @@ int Runner::runInitScript()
return 0;
}
int Runner::runEncFs(const char *password, int strlen) const
{
int aStdinPipe[2];
if (pipe(aStdinPipe) < 0) {
perror("allocating pipe for encfs");
return 0;
}
pid_t pid = fork();
if (pid == -1) {
fprintf(stderr, "Runner: Failed to fork\n");
return 0;
}
char buf[20];
snprintf(buf, sizeof(buf), "%d", m_jailId);
const std::string jailName(buf);
char *curDir = getcwd(nullptr, 0);
const std::string curDirStr(curDir);
free(curDir);
std::string jaildir = curDirStr + "/" + jailName;
if (pid == 0) { // we are the child
// redirect stdin to our pipe
if (dup2(aStdinPipe[PIPE_READ], STDIN_FILENO) == -1) {
exit(errno);
}
// these are for use by parent only
close(aStdinPipe[PIPE_READ]);
close(aStdinPipe[PIPE_WRITE]);
// close(aStdoutPipe[PIPE_READ]);
// close(aStdoutPipe[PIPE_WRITE]);
close(STDOUT_FILENO);
close(STDERR_FILENO);
char *arguments[7];
arguments[0] = const_cast<char*>("/usr/bin/encfs");
arguments[1] = const_cast<char*>("-f"); // foreground. Don't fork.
arguments[2] = const_cast<char*>("--public");
arguments[3] = const_cast<char*>("--stdinpass");
std::string backend = curDirStr + "/." + jailName;
arguments[4] = const_cast<char*>(backend.c_str());
arguments[5] = const_cast<char*>(jaildir.c_str());
arguments[6] = 0;
execv(arguments[0], arguments);
}
else {// we are the parent (pid = child's pid)
close(aStdinPipe[PIPE_READ]);
// monitor the process, if it dies we should die too.
new std::thread(waitForEncFs, pid);
bool ok = false;
// next we wait for the target dir to actually become a mount.
dev_t oldDevice = 0;
for (int i = 0; i < 200; ++i) { // max 15 sec
struct stat targetDir;
if (stat(jaildir.c_str(), &targetDir)) {
printf("stat of encrypted dir failed '%s'\n", jaildir.c_str());
exit(1);
}
if (i == 0) {
// On first loop, send the password to be 100%
// certain that the mount hasn't done anything yet.
// then remember the device this dir is on and if that
// changes then we know that the mount has succeeded.
oldDevice = targetDir.st_dev;
write(aStdinPipe[PIPE_WRITE], password, strlen);
write(aStdinPipe[PIPE_WRITE], "\n", 1);
}
else if (oldDevice != targetDir.st_dev) {
ok = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(75));
}
close(aStdinPipe[PIPE_WRITE]);
if (!ok) {
printf("EncFS never did its thing. Wrong password?\n");
exit(2);
}
}
/*
* We need to kill it later.
* Which means we need to have another process that actually has the rights
* to kill it we can talk to.
* So, a pipe and a simple process that will 'kill' the encfs process later,
* on command it is!
*/
const auto encFsPid = pid;
int encFsWatchdogPipe[2];
if (pipe(encFsWatchdogPipe) < 0) {
perror("allocating pipe for encfs");
kill(encFsPid, SIGTERM);
return 0;
}
pid = fork();
if (pid == -1) {
fprintf(stderr, "Runner: Failed to fork\n");
return 0;
}
else if (pid == 0) {
close(encFsWatchdogPipe[PIPE_WRITE]);
renameThisProcess(m_processName, m_processNameSize, "watchdog-encfs");
char buf[10];
do {
auto size = read(encFsWatchdogPipe[PIPE_READ], &buf, sizeof(buf));
if (size < 0)
exit(0);
if (size >= 2) {
kill(encFsPid, SIGTERM);
exit(0);
}
} while(true);
}
close(encFsWatchdogPipe[PIPE_READ]);
return encFsWatchdogPipe[PIPE_WRITE];
}
void renameThisProcess(char *nameBlob, int blobSize, const char *newName)
{
assert(nameBlob);
+3
View File
@@ -34,8 +34,11 @@ private:
void mkdirs(const std::filesystem::path &dir) const;
int runInitScript();
int runEncFs(const char *password, int strlen) const;
const int m_outputFD;
uint32_t m_ownerUid = 0;
uint32_t m_jailId = 0;
Message m_message;
char *m_processName = nullptr;
+4 -2
View File
@@ -7,7 +7,7 @@
#include <signal.h>
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QApplication>
// #define DEBUG_MESSAGE
@@ -163,8 +163,10 @@ static void mainListener(int x, char **y, int inputId, int outputId)
return;
}
QCoreApplication app(x, y);
QApplication app(x, y);
app.setApplicationName("isolation-runner");
app.setQuitOnLastWindowClosed(false); // avoid magic behavior
std::srand(std::time(0));
QCommandLineParser parser;
parser.addHelpOption(); // allows users to get an overview of options