Skip to content

Build image from source #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

Build image from source #234

wants to merge 10 commits into from

Conversation

Genva
Copy link

@Genva Genva commented Jun 14, 2025

As proposed in #233. Building images using the official Bitwarden repository.

@FingerlessGlov3s
Copy link

FingerlessGlov3s commented Jun 14, 2025

I took from what you did but changed a few bits. You'll need to ignore some bits, as I changed them so I can request the version I want to build, as I have a wrapper script that calls this as I had issues where the HTTP API version didn't match what needed to be built. My wrapper script, automates the updating process effectively.

If we move to this new method, the bitBetter C# program is no longer required, just the licenseGen. So any code regarding that can be removed from build.sh, and the c# project too.

All we'd need to do now is

  • Generate keys if needed
  • Clone the git repo at the right tag
  • Modify/Change the parts of the certificate code/file
  • Build containers
  • Add extra container tags
  • remove old BitBetter containers should they exis

Use licenseGen to generate any license files.

Thoughts?

How I run the script, working dir is /opt/bitwarden/

COREVER=`cat bitwarden.sh | grep COREVERSION= | cut -d '"' -f 2`
/opt/bitbetter/my-build.sh $COREVER
#!/bin/sh

DIR=`dirname "$0"`
DIR=`exec 2>/dev/null;(cd -- "$DIR") && cd -- "$DIR"|| cd "$DIR"; unset PWD; /usr/bin/pwd || /bin/pwd || pwd`
BW_VERSION=$1

echo "Building Api,Identity for BitWarden version $BW_VERSION (BitBetter)"

# If there aren't any keys, generate them first.
[ -e "$DIR/.keys/cert.cert" ] || "$DIR/.keys/generate-keys.sh"

# Prepare Bitwarden repository
rm -rf $DIR/server
git clone --branch "v${BW_VERSION}" --depth 1 https://github.com/bitwarden/server.git $DIR/server
old_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/server/src/Core/licensing.cer | cut -d= -f2 | tr -d ':')
new_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/.keys/cert.cert | cut -d= -f2 | tr -d ':')
cp $DIR/.keys/cert.cert $DIR/server/src/Core/licensing.cer

# Optional, has actually no effect
sed -i -e "s/$old_thumbprint/$new_thumbprint/g" $DIR/server/src/Core/Services/Implementations/LicensingService.cs
# Enable loose files for API, so Core.dll is accessible
sed -i -e 's/PublishSingleFile=true/PublishSingleFile=false/g' $DIR/server/src/Api/Dockerfile

docker build --no-cache --build-arg BITWARDEN_TAG=ghcr.io/bitwarden/api:$BW_VERSION --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Api/Dockerfile -t bitbetter/api:$BW_VERSION
if [ $? -ne 0 ]; then
  echo "Error: bitwarden/api build container failed."
  exit 1
fi

docker build --no-cache --build-arg BITWARDEN_TAG=ghcr.io/bitwarden/identity:$BW_VERSION --label com.bitwarden.product="bitbetter" $DIR/server -f $DIR/server/src/Identity/Dockerfile -t bitbetter/identity:$BW_VERSION
if [ $? -ne 0 ]; then
  echo "Error: bitwarden/identity container failed."
  exit 1
fi

docker tag bitbetter/api:$BW_VERSION bitbetter/api:latest
docker tag bitbetter/identity:$BW_VERSION bitbetter/identity:latest

# Remove old instances of the image after a successful build.
ids=$( docker images bitbetter/* | grep -E -v -- "CREATED|latest|${BW_VERSION}" | awk '{ print $3 }' )
[ -n "$ids" ] && docker rmi $ids || true

@Ayitaka
Copy link
Contributor

Ayitaka commented Jun 15, 2025

In case this is useful to anyone, on Debian 11 (Bullseye) you will get an error (for both certs) when running this, presumably because Debian 11 uses openssl 1.1.1w (Bullseye LTS expires next year):

# openssl x509 -fingerprint -noout -in server/src/Core/licensing.cer
unable to load certificate
140685837784384:error:0909006C:PEM routines:get_name:no start line:../crypto/pem/pem_lib.c:745:Expecting: TRUSTED CERTIFICATE

Piping it back through openssl to convert the cert seemed to produce the correct results:

# openssl x509 -inform DER -in server/src/Core/licensing.cer | openssl x509 -fingerprint -noout
SHA1 Fingerprint=B3:48:76:43:9F:CD:A2:84:65:05:B2:EF:BB:A6:C4:A9:51:31:3E:BE

Results from Debian 12 (Bookworm):

# openssl x509 -fingerprint -noout -in server/src/Core/licensing.cer
SHA1 Fingerprint=B3:48:76:43:9F:CD:A2:84:65:05:B2:EF:BB:A6:C4:A9:51:31:3E:BE

# openssl x509 -inform DER -in server/src/Core/licensing.cer | openssl x509 -fingerprint -noout
SHA1 Fingerprint=B3:48:76:43:9F:CD:A2:84:65:05:B2:EF:BB:A6:C4:A9:51:31:3E:BE

@Talkabout
Copy link

Hi guys,

I can confirm that this PR allows to successfully build bitwarden images for 2025.6.1. Thanks a lot @Genva !

Bye

Copy link
Contributor

@Fly7113 Fly7113 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think we can remove the files that are not included anymore in the new build: I think the whole src/bitBetter folder can be removed? I'm not really sure tho.
Nice addition the set -e line.
Also thanks to @FingerlessGlov3s for the suggestions.
A final note: do we want to support the old openssl1.1.1w version included in Bullseye LTS? Personally I'd advocate against it, since openssl 1.1.1 already reached EOL.

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 15, 2025

After further inspection of the code I am quite confident that the entirety of the src/bitBetter folder can now be removed.
At this point I think that the work left to do in this PR consists of:

  • Adjusting the pull strategy and the build-args passed to docker build, as suggested in the code review I submitted
  • Removing the unused code after making sure it's really not needed (src/bitBetter folder)
  • checking that we can retrieve the informations needed to generate the license also from the same individual file that now exists in the api container

The last point will consist in modifying some parts of the Program.cs file located in src/licenseGen, making it point to /app/Api (or whichever file contains the whole logic) instead of /app/Core.dll.

If everyone agrees then I kindly ask @Genva if you are able to update the PR to reflect the changes proposed in the first two points and submit a draft to solve the third point so that we can also help adjusting the code and testing out everything.

@Genva
Copy link
Author

Genva commented Jun 16, 2025

The licenseGen tool will now either use the Core.dll if present or extract it from the Api single file application. Like this disabling the single file parameter is not necessary anymore.

I kept the thumbprint replacement inside the bash.sh even though it should not be necessary either. It's only used to get the certificate out of the certificate store under Windows, as you can see here: CoreHelpers.cs
By removing the replacement, we would avoid any openssl issues. Thoughts?

Copy link
Contributor

@Fly7113 Fly7113 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the linked resources that use the cert's thumbprint.
If I read the code correctly, the thumbprint is used to retrieve the certificate only in not-self-hosted instances.
That said I think we could try to remove that part and test the code.
Kudos for the modified Program.cs retaining the retro compatibility with loose-files bitwarden images!

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 16, 2025

I tested out the latest code and unfortunately we were wrong about the certificate fingerprint: at some point it is very much needed! (I can't tell when unfortunately)

If you are interested, these are the logs of the Api container (Error due to a failed SSO login):
fail: Microsoft.AspNetCore.Server.Kestrel[13]
bitwarden-api  |       => SpanId:7ca9534fa84956d2, TraceId:a91ddfc1b40325afef41ccff2181174f, ParentId:0000000000000000 => ConnectionId:0HNDCTFIQVME9 => RequestPath:/organizations/domain/sso/verified RequestId:0HNDCTFIQVME9:00000001
bitwarden-api  |       Connection id "0HNDCTFIQVME9", Request id "0HNDCTFIQVME9:00000001": An unhandled exception was thrown by the application.
bitwarden-api  |       System.Exception: Invalid licensing certificate.
bitwarden-api  |          at Bit.Core.Services.LicensingService..ctor(IUserRepository userRepository, IOrganizationRepository organizationRepository, IMailService mailService, IWebHostEnvironment environment, ILogger`1 logger, IGlobalSettings globalSettings, ILicenseClaimsFactory`1 organizationLicenseClaimsFactory, ILicenseClaimsFactory`1 userLicenseClaimsFactory) in /source/src/Core/Services/Implementations/LicensingService.cs:line 77
bitwarden-api  |          at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
bitwarden-api  |          at System.Reflection.MethodBaseInvoker.InvokeWithManyArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
bitwarden-api  |          at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
bitwarden-api  |          at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
bitwarden-api  |          at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
bitwarden-api  |          at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
bitwarden-api  |          at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
bitwarden-api  | fail: Microsoft.AspNetCore.Server.Kestrel[13]

In conclusion we have to revert the last commit (d8465e1)

I tested out the script after adding back the following lines to build.sh:

old_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/server/src/Core/licensing.cer | cut -d= -f2 | tr -d ':')
new_thumbprint=$(openssl x509 -fingerprint -noout -in $DIR/.keys/cert.cert | cut -d= -f2 | tr -d ':')
sed -i -e "s/$old_thumbprint/$new_thumbprint/g" $DIR/server/src/Core/Services/Implementations/LicensingService.cs

Now everything seems to work:

  • SSO and regular login to existing organisations
  • Generation and validation of organisation license
  • Generation and validation of user license

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 16, 2025

Also I think we need to update the .gitignore so that the server folder fetched from the bitwarden/server repo is not tracked by git

@Genva
Copy link
Author

Genva commented Jun 16, 2025

I reverted the latest commit. As a matter of fact, here they validate the thumbprint of the loaded certificate against the hardcoded one. Must have overlooked it when I checked the constructor.

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 16, 2025

Great catch. As of now I think the PR can be considered done, as long as no one else is willing to test this implementation more thoroughly.

Just a minor note at this point: it might be beneficial to edit the comment about the replacing of the cert's thumbprint having "no actual effect", to avoid anyone falsely believing that those lines are in fact not useful.

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 16, 2025

Before calling it a day, I'd like to share a thought that I had in mind regarding this solution:

Until the last version the BitBetter code was in fact modifying the Core.dll files in the Api and Identity containers in order to replace the bundled certificate and its thumbprint. This of course was better than building everything from source since it needed fewer resources and was generally faster (it just needed to unpack, modify and repack just a single file, not the whole code).
Now of course we can't edit the single Core.dll file and we resort to simply building the whole image from source, taking for granted that the operation of unpacking-modifying-repacking the whole Api and Identity files would take as much resources and time as building them from source.
Now the question that arises is the following: are we really sure that the process of unpacking-modifying-repacking is really just as intensive and long as building the whole images from scratch like we thought? Or should we try to unpack-modify-repack the two files of the two images like it was done with Core.dll?

This of course would be not ideal since we spent the past few days coding and reviewing another solution, but I wonder if we should just ignore the other possibility.
I'd like to hear your thoughts about it as well. Maybe I am just hallucinating and the alternative solution I'm thinking about isn't possible at all :')

@Genva
Copy link
Author

Genva commented Jun 17, 2025

@Fly7113 You were right, I changed the comment. About modifying the single file application in a way, we did before. It might be possible, but I don't know how. If you are interested feel free to check it in a separate PR. For now, I'd take this solution as it works.

@Fly7113
Copy link
Contributor

Fly7113 commented Jun 17, 2025

@captainhook @h44z What's your take on this PR? Ready to be merged?

@captainhook
Copy link
Collaborator

captainhook commented Jun 17, 2025

@captainhook @h44z What's your take on this PR? Ready to be merged?

I'll run some tests on my end before I approve. Will invite a few others to test and approve since this is a big change.

Please kindly include comments in your testing including license gen/import as well as general use of the application.

@captainhook captainhook requested review from Fly7113, h44z and captainhook and removed request for FingerlessGlov3s June 17, 2025 09:22
Copy link
Contributor

@Fly7113 Fly7113 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests done:

  • Image patching: works as expected, both from fresh install and already patched with previous versions; build time is sensibly higher as expected (5-10 times higher than before, depending on machine specs);
  • User license generation: works as expected;
  • User license validation: works as expected, all premium features available;
  • Org license generation: works as expected;
  • Org license validation: works as expected, all premium features available (SSO, Key Connector, SCIM, AD);

@TheSp1der
Copy link

I can confirm that this method works to build the new images.
We might want to either detect and set the TARGETPLATFORM build environment variable or alert that it might need to be set by the user.
I also had to inject RUN git config --global --add safe.directory '*' into the Api and Identity dockerfiles to get it to build with kaniko, but that's pretty platform specific.

@captainhook
Copy link
Collaborator

Agreed on the TARGETPLATFORM - or set a default for amd64. Could anyone add this in please? I will run a test tomorrow - apologies for the delay. @h44z if you have any capacity to test, would be grateful since this is a large change.

@captainhook
Copy link
Collaborator

captainhook commented Jun 25, 2025

Merged #236 - not blocking. Please consider for main.

@captainhook captainhook mentioned this pull request Jun 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants