You've already forked isolationRunner
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:
+2
-2
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user