When using Ghostscript on Ubuntu, be aware that Ubuntu added an Apparmor config for Ghostscript and it is not particularly sophisticated. Ask me how I found out. Well, I mean, this is the blog post about how I found out, so you don’t even need to ask anymore.

A client of mine was trying to use Ghostscript to create so-called ZUGFeRD invoices. It’s a german/french standard to create PDF invoices that contain an XML attachment that conforms to the EN 16931 standard for electronic invoicing. Exciting stuff, I know, but somehow I know more about this than I ever wanted to know.

The step of actually attaching the XML to the PDF (and making the PDF a valid PDF/A3 file) has always been a bit fiddly. So far, I am having most luck with Mustangproject but that’s Java and not very elegant, is it?

Turns out, Ghostscript officially comes with a script (one could say, a post-script) to do exactly that.

It is documented but it is rather fiddly to use.

Or rather, I could not get it to work at all. The official incantation from the documentation looks like this:

gs
--permit-file-read=/usr/home/me/zugferd/ \
-sDEVICE=pdfwrite \
-dPDFA=3 \
-sColorConversionStrategy=RGB \
-sZUGFeRDXMLFile=/usr/home/me/zugferd/invoice.xml \
-sZUGFeRDProfile=/usr/home/me/zugferd/default_rgb.icc \
-sZUGFeRDVersion=2p1 \
-sZUGFeRDConformanceLevel=BASIC \
-o /usr/home/me/zugferd/output.pdf \
/usr/home/me/zugferd/zugferd.ps \
/usr/home/me/zugferd/input.pdf

Note the --permit-file-read thing which is “relatively” new in Ghostscript - It, very much appreciatedly, no longer blindly trusts file inputs but wants to make sure you define what files you actually need. This is called SAFER mode, and was an option for a long time and is, for a couple of versions, now the default.

But I would alway, regardless of what I tried, get error messages. Interestingly, and I only later realized what that meant, the error seemed to come from the OS. The issue was that the script (the zugferd.ps file) tries to read the color profile and can’t.

So I started to dabble around. Now, I know absolutely nothing about Ghostscript or postscript, but while postscript looks absolutely alien to me, I could still, at least a little, figure out what the zugferd.ps script was trying to do.

Here’s the most minimalistic reproduction I was able to come up with:

gs -dBATCH -dNOPAUSE -dNOSAFER -dNODISPLAY -c "(default_rgb.icc) (r) file status = quit"

This would fail. Interestingly, if I replaced default_rbg.icc with, say a small text file called test.txt, it would work.

This didn’t seem right. I double/triple checked file access rights. I tried various combinations of paths and --permit-file-read and -dNOSAFER and nothing would really work.

So, it seemed to be extension specific, but I couldn’t find anything in the Ghostscript documentation about extension whitelists, and after all, given how much Ghostscript deals with color profiles, blocking *.icc would not make any sense.

So, I asked ChatGPT Kagi once more and somehow I found a pointer to Apparmor.

Yes, FUCKING Apparmor.

So, on Ubuntu 25.10, at least, there is an Apparmor config for the Ghostscript binary gs and in /etc/apparmor.d/tunables/gs, it defines the allowed extensions. On my machine, it looked like this:

#------------------------------------------------------------------
#    Copyright (C) 2025 Canonical Ltd.
#
#    Author: Giampaolo Fresi Roglia (gianz)
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of version 2 of the GNU General Public
#    License published by the Free Software Foundation.
#------------------------------------------------------------------
# vim: ft=apparmor

@{gs_file_ext}=[pP][dD][fF] [pP][sS] [eE][pP][sS] [eE][pP][sS][iI] [pP][nN][gG] [jJ][pP][gG] [jJ][pP][eE][gG] [pP][nN][mM] [tT][iI][fF] [tT][iI][fF][fF] [bB][mM][pP] [pP][cC][xX] [pP][sS][dD] [tT][xX][tT] [pP][xX][lL] [dD][oO][cC][xX] [xX][pP][sS]

include if exists <tunables/gs.d>

This is a weird way of defining file extensions, but who am I to argue. Notice any absences? Yip, no [iI][cC][cC] to be found anywhere.

Also, notably, no [xX][mM][lL] which is also needed for ZUGFeRD and if it were me I would probably also throw in some [rR][dD][fF] for good measure.

So, yeah, that’s a couple of hours of my life I won’t get back.

Now, I’m not sure where to report this to, but I will definitely try.

And, yes, after adding these things to the file, and reloading the apparmor service, I can now generate valid ZUGFeRD invoices with Ghostscript.