💾 Archived View for caseyrichins.online › logs › 2022-02-21_Gemini-in-docker.gmi captured on 2024-05-10 at 10:59:23. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-12-28)

-=-=-=-=-=-=-

Gemini Capsule in Docker

Published 2022-02-21

After learning about the protocol and playing around with Amfora and Lagrange on Linux I stared down the path of looking into servers to host my own capsule. I've tried MollyBrown and Agate. However, of the two MollyBrown was much simpler to setup. I have a working Docker configuration for Agate too, but in trying to use my own generated certificate I've not gotten it to work. Agate expects certificates to be in a DER format and converting my personally generated key & cert from PEM to DER format doesn't work. When I spin up agate it only works when I have agate auto generate a new certificate when starting, but this is not ideal for docker since it's expected for containers to be disposable. My other reason for going with MollyBrown is because of the support for client certificate authentication. So far as I can tell, either agate doesn't allow for client certs or I'm just not at the point where I understand agate enough to know how to configure them.

Now getting into the configuration and HowTo aspect of this post let me put out there first that in all the work I'm doing and documenting below is with a security frame of mind, since as a security professional in my daily life, my work is all about security and minimizing risk. This is the reason for how and why certain configurations were made. If anyone has any improvements to the steps outlined in terms of security to this post, I welcome the feedback. The setup below assumes a hardened docker host, but that is a different post in itself.

The instructions you see here I've also posted to the '/r/geminiprotocol' subreddit.

Gemini in Docker subreddit post

The Directory Tree

├── docker-compose.yml
├── mollybrown
│   ├── certs
│   │   ├── cert.pem
│   │   └── key.pem
│   ├── config
│   │   └── molly.conf
│   ├── content
│   │   ├── about.gmi
│   │   ├── books.gmi
│   │   ├── index.gmi
│   │   ├── logs
│   │   │   └── 2022-02-21_Gemini-in-docker.gmi
│   │   ├── pgp.gmi
│   │   ├── quotes.gmi
│   │   ├── secrets
│   │   │   └── index.gmi
│   │   └── telegram.gmi
│   ├── Dockerfile

First they "why" of the docker setup. Using a docker container you can isolate the Gemini server process and host server processes so that the only process running within the container is the Gemini server process, thus reducing attack surface for the Gemini server. It's not foolproof but it's better than hosting on a host machine that may be running other insecure processes. Second, the capsule can be made disposable in a sense that if you needed to move to a new host in case the host was compromised it's simply a matter of spinning up a new docker host. If there's ever an issue with the container you can destroy it and re-create it quickly.

You can host the files and configuration on your private git hub repository to get some version control over your setup and gem files.

The Molly Configuration

## Basic settings
#
#Port = 1965
Hostname = "domain.space"
CertPath = "/home/molly/ssl/cert.pem"
KeyPath = "/home/molly/ssl/key.pem"
#DocBase = "/var/gemini/"
#HomeDocBase = "users"
#GeminiExt = "gmi"
DefaultLang = "en"
AccessLog = "-"
ErrorLog = "-"
#ReadMollyFiles = true
#
## Directory listing
#
#DirectorySort = "Time"
#DirectoryReverse = true
#DirectoryTitles = true
#
## Dynamic content
#
#CGIPaths = [
#	"/var/gemini/cgi-bin",
#	"/var/gemini/users/*/cgi-bin/", # Unsafe!
#]
#
#[SCGIPaths]
#"/scgi-app-1/" = "/var/run/scgi1.sock"
#"/scgi-app-2/" = "/var/run/scgi2.sock"
#
## MIME type overrides
#
#[MimeOverrides]
#"atom.xml$" = "application/atom+xml"
#"rss.xml$" = "application/rss+xml"
#
## Redirects
#
#[TempRedirects]
#"/old/path/file.ext" = "/new/path/file.ext"
#[PermRedirects]
#"/old/path/file.ext" = "/new/path/file.ext"
#
## Certificate zones
#
[CertificateZones]
"^/secrets/" = [
	"d146953386694266175d10be3617427dfbeb751d1805d36b3c7aedd9de02d9af",
]
#"^/secure-zone-2/" = [
#	"d146953386694266175d10be3617427dfbeb751d1805d36b3c7aedd9de02d9af",
#	"786257797c871bf617e0b60acf7a7dfaf195289d8b08d1df5ed0e316092f0c8d",
#]

When setting up the configuration file for molly you'll want to edit the Access log and Error log lines to be defined as "-" so that they are written to stdout instead of to disk, this is important as you'll see later then we start the container with a read only file system.

If you are wanting to make specific pages or directories require the presentation of a client certificate you'll need to configure the [CertificateZones] with the path or file and the fingerprint of the client certificate to be used to access the document.

Building The image

FROM golang:alpine3.15 AS builder

ENV GOPATH /root/go 

RUN mkdir /root/go && go get tildegit.org/solderpunk/molly-brown

FROM alpine:latest

EXPOSE 1965

COPY --from=builder /root/go/bin/molly-brown /usr/sbin/molly-brown

RUN adduser -D -s /sbin/nologin molly && mkdir /home/molly/ssl /var/gemini

COPY --chown=molly:molly ./config/molly.conf /etc/molly.conf

COPY --chown=molly:molly ./certs/ /home/molly/ssl/

COPY --chown=molly:molly ./content/ /var/gemini/

RUN chown -R molly: /var/gemini

USER molly

CMD ["/usr/sbin/molly-brown"]

Using the Dockerfile sample from above you'll be able to build the container image that you'll use to run your Gemini capsule with your content within the docker image so that the container itself is disposable as well as a trusted source that can be set in read only mode to prevent file or configuration modification in the event the container is compromised.

Once you have your Dockerfile in a location similar to the directory tree in the previous section you can build it with your build command below, choosing your image name and tag.

docker build --force-rm -t mollytest:latest .

We define a user molly in the Dockerfile so that the container can drop privileges to allow the Gemini process to run as a non-root user, further allowing it to be run more securely. The process of building a working binary is done in different image and then copied to our image with the --from=builder flag to keep our production container small and clean of unnecessary items.

After building the image, if you wish to test you may do so with the following command. You will likely need to create a hosts file entry for the domain you configured in the molly.conf configuration file above so that your computer will on try to go out to the internet to find the capsule that you'll start on your local computer. We specify --rm so that the container will be removed automatically after it is stopped. Once running your should be able to visit the domain you configured in the molly.conf file above and see the content you created.

docker run --rm -p 127.0.0.1:1965:1965 -d --name=gemini mollytest:latest

Once the image has been built it will only exist on your local machine, if you wish to use it on your production server you'll need to tag & push it DockerHub or other image repository that that It can be pulled down and started on your production instance. dockerhub is the repo name on DockerHub that you want to push to.

docker tag mollytest:latest dockerhub/gemini_app:latest

docker push dockerhub/gemini_app:latest

Docker Compose

version: "3"

services:
  gemini:
    container_name: gemini
    image: dockerhub/gemini_app:latest
    read_only: true
    ports:
      - "1965:1965"
    restart: always
    user: molly
    networks: 
      - gemini

networks:
  gemini:

Once you're ready to start a production instance you can copy the configuration above into a docker-compse.yml file on your production server and run the docker-compose up command (provided you have docker-compose installed on your server) to start the environment. Port 1965 on your host server will be mapped to port 1965 in the container. You should be able to browse the site with any Gemini browser if your domain dns is pointing to the server.

What about a development environment?

Now you may be wondering what do I do about a development environment?

I want to be able to to make changes and preview my work without having to build a new container every time.

I hear you, the answer is simple, making a development image but instead of copying our content files into the container, we create a bind mount that allows us to make changes to files on our local machine and refresh the browser to see the changes, we are also able to make sure our image is working as we expect with user privileges for the server process and still have the container run in read-only mode while keeping our mount writable.

Below is the development Dockerfile called Dockerfile.dev that we use to build the image, you'll notice that biggest difference is that we have no line copying our content into the container.

FROM golang:alpine3.15 AS builder

ENV GOPATH /root/go 

RUN mkdir /root/go && go get tildegit.org/solderpunk/molly-brown

FROM alpine:latest

EXPOSE 1965

COPY --from=builder /root/go/bin/molly-brown /usr/sbin/molly-brown

RUN adduser -D -s /sbin/nologin molly && mkdir /home/molly/ssl /var/gemini

COPY --chown=molly:molly ./certs/ /home/molly/ssl/

COPY --chown=molly:molly ./config/molly.conf /etc/molly.conf

RUN chown -R molly:molly /var/gemini

USER molly

CMD ["/usr/sbin/molly-brown"]

We build the image with the command below:

docker build --force-rm -t mollytest:dev -f Dockerfile.dev .

Followed by running the docker run command to start our development container. Now files can be edited within your content directory have changes reflect immediately without having to rebuild your container image after every change. We start it bound to localhost so that we aren't opening our local machine up to a port that doesn't need to be publicly open.

docker run --rm --mount type=bind,source="$(pwd)"/content,target=/var/gemini/ -p 127.0.0.1:1965:1965 -d --name=gemini --read-only mollytest:dev

If you have questions or need clarification on anything I'm happy to assist.

Mission Logs

Home