About me: My name is Solène Rapenne, pronouns she/her. I like learning and sharing knowledge. Hobbies: '(BSD OpenBSD h+ Lisp cmdline gaming internet-stuff). I love percent and lambda characters. OpenBSD developer solene@.

Contact me: solene on Freenode, solene+www at dataswamp dot org or solene@bsd.network (mastodon). If for some reason you want to give me some money, I accept paypal at the address donate@perso.pw.

Vger security analysis

Written by Solène, on 14 January 2021.
Tags: #vger #gemini #security

Comments on Mastodon

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.


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.

      * use chroot() if an 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 root");
                     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 system", user);
                     err(1, "finding user");
             /* chroot worked? */
             if (chroot(path) != 0) {
                     syslog(LOG_DAEMON, "the chroot_dir %s can't be used for chroot", path);
                     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) failed",
                            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.

 #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");

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.

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));
             case 'l':
                     estrlcpy(lang, "lang=", sizeof(lang));
                     estrlcat(lang, optarg, sizeof(lang));
             case 'm':
                     estrlcpy(default_mime, optarg, sizeof(default_mime));
             case 'u':
                     estrlcpy(user, optarg, sizeof(user));
             case 'v':
                     virtualhost = 1;
             case 'i':
                     doautoidx = 1;

      * do chroot if an 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.

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.

OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | md5)
if ! [ $OUT = "770a987b8f5cf7169e6bc3c6563e1570" ]
	echo "error"
	exit 1

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.

ls *.c | entr make test

Realtime integration tests? :)


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.

Port of the week: Lagrange

Written by Solène, on 02 January 2021.
Tags: #portoftheweek #gemini

Comments on Mastodon

Today's Port of the Week is about Lagrange, a gemini web browser.

Lagrange official website

Information about the Gemini protocol

Curated list of Gemini clients

Lagrange is the finest browser I ever used and it's still brand new. I imported it into OpenBSD and so it will be available starting from OpenBSD 6.9 releases.

Screenshot of the web browser in action with dark mode, it supports left and right side panels.

Lagrange is fantastic in the way it helps the user with the content browsed.

  • Links already visited display the last visited date
  • Subscription on page without RSS is possible for pages respecting a specific format (most of gemini space does)
  • Easy management of client certificates, used for authentication
  • In-page image loading, video watching and sound playing
  • Gopher support
  • Table of content displayed generated from headings
  • Keyboard navigation
  • Very light (dependencies, memory footprint, cpu usage)
  • Smooth scrolling
  • Dark and light modes
  • Much more

If you are interested into Gemini, I highly recommend this piece of software as a browser.

In case you would like to host your own Gemini content without requiring infrastructure, some community servers are offering hosting through secure sftp transfers.

Si3t.ch community Gemini hosting

Un bon café !

Once you get into Gemini space, I recommend the following resources:

CAPCOM feed agregator, a great place to meet new authors

GUS: a search engine

Vger gemini server can now redirect

Written by Solène, on 02 January 2021.
Tags: #gemini

Comments on Mastodon

I added a new feature to Vger gemini server.

Vger git repository

The protocol supports status code including redirections, Vger had no way to know if an user wanted to redirect a page to another. The redirection litteraly means "You asked for this content but it is now at that place, load it from there".

To keep it with vger Unix way, a redirection is done using a symbolic link:

The following command would redirect requests from gemini://perso.pw/blog/index.gmi to gemini://perso.pw/blog/index.gmi:

ln -s "gemini://perso.pw/capsule/index.gmi" blog/index.gmi

Unfortunately, this doesn't support globbing, in other words it is not possible to redirect everything from `/blog/` to `/capsule/` without creating a symlink for all previous resources to their new locations.

How to deploy Vger gemini server on OpenBSD

Written by Solène, on 30 November 2020.
Tags: #gemini #openbsd

Comments on Mastodon


In this article I will explain how to install and configure Vger, a gemini server.

What is the gemini protocol

Short introduction about Gemini: it's a very recent protocol that is being simplistic and limited. Keys features are: pages are written in markdown like, mandatory TLS, no header, UTF-8 encoding only.

Vger program

Vger source code

I wrote Vger to discover the protocol and the Gemini space. I had a lot of fun with it, it was the opportunity for me to rediscover the C language with a better approach. The sources include a full test suite. This test suite was unvaluable for the development process.

Vger was really built with security in mind from the first lines of code, now it offers the following features:

  • chroot and privilege dropping, and on OpenBSD it uses unveil/pledge all the time
  • virtualhost support
  • language selection
  • MIME detection
  • handcrafted man page, OpenBSD quality!

The name Vger is a reference to the 1979 first Star Trek movie.

Star Trek: The Motion Picture

Install Vger

Compile vger.c using clang or gcc

$ make
# install -o root -g bin -m 755 vger /usr/local/bin/vger

Vger receives requests on stdin and gives the result on stdout. It doesn't take account of the hostname given but a request MUST start with `gemini://`.

vger official homepage

Setup on OpenBSD

Create directory /var/gemini/, files will be served from there.

Create the `_gemini` user:

useradd -s /sbin/nologin _gemini

Configure vger in /etc/inetd.conf

11965 stream tcp nowait _gemini /usr/local/bin/vger vger

Inetd will run vger` with the _gemini user. You need to take care that /var/gemini/ is readable by this user.

inetd is a wonderful daemon listening on ports and running commands upon connections. This mean when someone connects on the port 11965, inetd will run vger as _gemini and pass the network data to its standard input, vger will send the result to the standard output captured by inetd that will transmit it back to the TCP client.

Tell relayd to forward connections in relayd.conf

log connection
relay "gemini" {
    listen on port 1965 tls
    forward to port 11965

Make links to the certificates and key files according to relayd.conf documentation. You can use acme / certbot / dehydrate or any "Let's Encrypt" client to get certificates. You can also generate your own certificates but it's beyond the scope of this article.

# ln -s /etc/ssl/acme/cert.pem /etc/ssl/\:1965.crt
# ln -s /etc/ssl/acme/private/privkey.pem /etc/ssl/private/\:1965.key

Enable inetd and relayd at boot and start them

# rcctl enable relayd inetd
# rcctl start relayd inetd

From here, what's left is populating /var/gemini/ with the files you want to publish, the `index.md` file is special because it will be the default file if no file are requests.