| Title: Vger security analysis | |
| Author: Solène | |
| Date: 14 January 2021 | |
| Tags: vger gemini security | |
| Description: | |
| I would like to share about Vger internals in regards to how the | |
| security was thought to protect vger users and host systems. | |
| Vger code repository | |
| # Thinking about security first | |
| I claim about security in Vger as its main feature, I even wrote Vger | |
| to have a secure gemini server that I can trust. Why so? It's written | |
| in C and I'm a beginner developer in this language, this looks like a | |
| scam. | |
| I chose to follow the best practice I'm aware of from the very first | |
| line. My goal is to be sure Vger can't be used to exfiltrate data from | |
| the host on which it runs or to allow it to run arbirary command. While | |
| I may have missed corner case in which it could crash, I think a crash | |
| is the worse that can happen with Vger. | |
| ## Smallest code possible | |
| Vger doesn't have to manage connections or TLS, this was a lot of code | |
| already removed by this design choice. There are better tools which are | |
| exactly made for this purpose, so it's time to reuse other people good | |
| work. | |
| ## Inetd and user | |
| Vger is run by inetd daemon, allowing to choose the user running vger. | |
| Using a dedicated user is always a good idea to prevent any harm in | |
| case of issue, but it's really not sufficient to protect vger to behave | |
| badly. | |
| Another kind of security benefit is that vger runtime isn't looping | |
| like a daemon awaiting new connections. Vger accept a request, read a | |
| file if exist and gives its result and terminates. This is less error | |
| prone because no variable can be reused or tricked after a loop that | |
| could leave the code in an inconsistent or vulnerable state. | |
| ## Chroot | |
| A critical vger feature is the ability to chroot into a directory, | |
| meaning the directory is now seen as the root of the file system | |
| (/var/gemini would be seen as /) and prevent vger to escape it. In | |
| addition to the chroot feature, the feature allow vger to drop to an | |
| unprivileged user. | |
| ```C code showing the chroot feature | |
| /* | |
| * use chroot() if a user is specified requires root user to be | |
| * running the program to run chroot() and then drop privileges | |
| */ | |
| if (strlen(user) > 0) { | |
| /* is root? */ | |
| if (getuid() != 0) { | |
| syslog(LOG_DAEMON, "chroot requires program to be run as r… | |
| errx(1, "chroot requires root user"); | |
| } | |
| /* search user uid from name */ | |
| if ((pw = getpwnam(user)) == NULL) { | |
| syslog(LOG_DAEMON, "the user %s can't be found on the syst… | |
| err(1, "finding user"); | |
| } | |
| /* chroot worked? */ | |
| if (chroot(path) != 0) { | |
| syslog(LOG_DAEMON, "the chroot_dir %s can't be used for ch… | |
| err(1, "chroot"); | |
| } | |
| chrooted = 1; | |
| if (chdir("/") == -1) { | |
| syslog(LOG_DAEMON, "failed to chdir(\"/\")"); | |
| err(1, "chdir"); | |
| } | |
| /* drop privileges */ | |
| if (setgroups(1, &pw->pw_gid) || | |
| setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || | |
| setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) { | |
| syslog(LOG_DAEMON, "dropping privileges to user %s (uid=%i… | |
| user, pw->pw_uid); | |
| err(1, "Can't drop privileges"); | |
| } | |
| } | |
| ``` | |
| ## No use of third party libs | |
| Vger only requires standard C includes, this avoid leaving trust to | |
| dozens of developers using fragile or barely tested code. | |
| ## OpenBSD specific code | |
| In addition to all the previous security practices, OpenBSD is offering | |
| a few functions to help restricting a lot what Vger can do. | |
| The first function is pledge, allowing to restrict the system calls | |
| that can happen within the code itself. The current syscalls allowed in | |
| vger are related to the categories "rpath" and "stdio", basically | |
| standard input/output and reading files/directories only. This mean | |
| after pledge() is called, if any syscall not in those two categories is | |
| used, vger will be killed and a pledge error will be reported in the | |
| logs. | |
| The second function is unveil, which will basically restrict access to | |
| the filesystem to anything but what you list, with the permission. | |
| Currently, vger only allows file access in read-only mode in the base | |
| directory used to serve files. | |
| Here is an extract of the code relative to the OpenBSD specific code. | |
| With unveil available everywhere chroot wouldn't be required. | |
| ```C code with OpenBSD specific code | |
| #ifdef __OpenBSD__ | |
| /* | |
| * prevent access to files other than the one in path | |
| */ | |
| if (chrooted) { | |
| eunveil("/", "r"); | |
| } else { | |
| eunveil(path, "r"); | |
| } | |
| /* | |
| * prevent system calls other parsing queryfor fread file and | |
| * write to stdio | |
| */ | |
| if (pledge("stdio rpath", NULL) == -1) { | |
| syslog(LOG_DAEMON, "pledge call failed"); | |
| err(1, "pledge"); | |
| } | |
| #endif | |
| ``` | |
| # The least code before dropping privileges | |
| I made my best to use the least code possible before reducing Vger | |
| capabilities. Only the code managing the parameters is done before | |
| activating chroot and/or unveil/pledge. | |
| ```C code showing the parameters parsing | |
| int | |
| main(int argc, char **argv) | |
| { | |
| char request [GEMINI_REQUEST_MAX] = {'\0'}; | |
| char hostname [GEMINI_REQUEST_MAX] = {'\0'}; | |
| char uri [PATH_MAX] = {'\0'}; | |
| char user [_SC_LOGIN_NAME_MAX] = ""; | |
| int virtualhost = 0; | |
| int option = 0; | |
| char *pos = NULL; | |
| while ((option = getopt(argc, argv, ":d:l:m:u:vi")) != -1) { | |
| switch (option) { | |
| case 'd': | |
| estrlcpy(chroot_dir, optarg, sizeof(chroot_dir)); | |
| break; | |
| case 'l': | |
| estrlcpy(lang, "lang=", sizeof(lang)); | |
| estrlcat(lang, optarg, sizeof(lang)); | |
| break; | |
| case 'm': | |
| estrlcpy(default_mime, optarg, sizeof(default_mime)); | |
| break; | |
| case 'u': | |
| estrlcpy(user, optarg, sizeof(user)); | |
| break; | |
| case 'v': | |
| virtualhost = 1; | |
| break; | |
| case 'i': | |
| doautoidx = 1; | |
| break; | |
| } | |
| } | |
| /* | |
| * do chroot if a user is supplied run pledge/unveil if OpenBSD | |
| */ | |
| drop_privileges(user, chroot_dir); | |
| ``` | |
| # The Unix way | |
| Unix is made of small component that can work together as small bricks | |
| to build something more complex. Vger is based on this idea by | |
| delegating the listening daemon handling incoming requests to another | |
| software (let's say relayd or haproxy). And then, what's left from the | |
| gemini specs once you delegate TLS is to take account of a request and | |
| return some content, which is well suited for a program accepting a | |
| request on its standard input and giving the result on standard ouput. | |
| Inetd is a key here to make such a program compatible with a daemon | |
| like relayd or haproxy. When a connection is made into the TLS | |
| listening daemon, a local port will trigger inetd that will run the | |
| command, passing the network content to the binary into its stdin. | |
| # Fine grained CGI | |
| CGI support was added in order to allow Vger to make dynamic content | |
| instead of serving only static files. It has a fine grained control, | |
| you can allow only one file to be executable as a CGI or a whole | |
| directory of files. When serving a CGI, vger forks, a pipe is opened | |
| between the two processes and a process is using execlp to run the cgi | |
| and transmit its output to vger. | |
| # Using tests | |
| From the beginning, I wrote a set of tests to be sure that once a kind | |
| of request or a use case work I can easily check I won't break it. This | |
| isn't about security but about reliability. When I push a new version | |
| on the git repository, I am absolutely confident it will work for the | |
| users. It was also an invaluable help for writing Vger. | |
| As vger is a simple binary that accept data in stdin and output data on | |
| stdout, it is simple to write tests like this. The following example | |
| will run vger with a request, as the content is local and within the | |
| git repository, the output is predictable and known. | |
| ```Shell command to run vger for testing purpose using a pipe | |
| printf "gemini://host.name/autoidx/\r\n" | vger -d var/gemini/ | |
| ``` | |
| From here, it's possible to build an automatic test by checking the | |
| checksum of the output to the checksum of the known correct output. Of | |
| course, when you make a new use case, this requires manually generating | |
| the checksum to use it as a comparison later. | |
| ```Shell command comparing vger output to a checksum | |
| OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | md… | |
| if ! [ $OUT = "770a987b8f5cf7169e6bc3c6563e1570" ] | |
| then | |
| echo "error" | |
| exit 1 | |
| fi | |
| ``` | |
| At this time, vger as 19 use case in its test suite. | |
| By using the program `entr` and a Makefile to manage the build process, | |
| it was very easy to trigger the testing process while working on the | |
| source code, allowing me to check the test suite only by saving my | |
| current changes. Anytime a .c file is modified, entr will trigger a | |
| make test command that will be displayed in a dedicated terminal. | |
| ```shell command using the command "entr" to auto rebuild the project | |
| ls *.c | entr make test | |
| ``` | |
| Realtime integration tests? :) | |
| # Conclusion | |
| By using best practices, reducing the amount of code and using only | |
| system libraries, I am quite confident about Vger good security. The | |
| only real issue could be to have too many connections leading to a | |
| quite high load due to inetd spawning new processes and doing a denial | |
| of services. This could be avoided by throttling simultaneous | |
| connection in the TLS daemon. | |
| If you want to contribute, please do, and if you find a security issue | |
| please contact me, I'll be glad to examine the issue. |